From 8697c15fe63e4379d0e773a7436128e2fe27c54a Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Fri, 22 Oct 2021 07:45:56 +0200 Subject: [PATCH] Nextcloud-based OCM Share Manager (#2163) --- .../unreleased/nextcloud-ocm-share-manager.md | 7 + pkg/ocm/share/manager/loader/loader.go | 1 + pkg/ocm/share/manager/nextcloud/nextcloud.go | 481 ++++++++++ .../nextcloud/nextcloud_server_mock.go | 114 +++ .../manager/nextcloud/nextcloud_suite_test.go | 31 + .../share/manager/nextcloud/nextcloud_test.go | 832 ++++++++++++++++++ 6 files changed, 1466 insertions(+) create mode 100644 changelog/unreleased/nextcloud-ocm-share-manager.md create mode 100644 pkg/ocm/share/manager/nextcloud/nextcloud.go create mode 100644 pkg/ocm/share/manager/nextcloud/nextcloud_server_mock.go create mode 100644 pkg/ocm/share/manager/nextcloud/nextcloud_suite_test.go create mode 100644 pkg/ocm/share/manager/nextcloud/nextcloud_test.go diff --git a/changelog/unreleased/nextcloud-ocm-share-manager.md b/changelog/unreleased/nextcloud-ocm-share-manager.md new file mode 100644 index 0000000000..0d20e86dbb --- /dev/null +++ b/changelog/unreleased/nextcloud-ocm-share-manager.md @@ -0,0 +1,7 @@ +Enhancement: Nextcloud-based share manager for pkg/ocm/share + +Note that pkg/ocm/share is very similar to pkg/share, +but it deals with cs3/sharing/ocm +whereas pkg/share deals with cs3/sharing/collaboration + +https://github.com/cs3org/reva/pull/2163 diff --git a/pkg/ocm/share/manager/loader/loader.go b/pkg/ocm/share/manager/loader/loader.go index f3f7587308..6c327b184f 100644 --- a/pkg/ocm/share/manager/loader/loader.go +++ b/pkg/ocm/share/manager/loader/loader.go @@ -21,5 +21,6 @@ package loader import ( // Load core share manager drivers. _ "github.com/cs3org/reva/pkg/ocm/share/manager/json" + _ "github.com/cs3org/reva/pkg/ocm/share/manager/nextcloud" // Add your own here ) diff --git a/pkg/ocm/share/manager/nextcloud/nextcloud.go b/pkg/ocm/share/manager/nextcloud/nextcloud.go new file mode 100644 index 0000000000..b00673f7cf --- /dev/null +++ b/pkg/ocm/share/manager/nextcloud/nextcloud.go @@ -0,0 +1,481 @@ +// 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" + + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/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/ocm/share" + "github.com/cs3org/reva/pkg/ocm/share/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "google.golang.org/genproto/protobuf/field_mask" +) + +func init() { + registry.Register("nextcloud", New) +} + +// Manager is the Nextcloud-based implementation of the share.Manager interface +// see https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +type Manager 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"` + MockHTTP bool `mapstructure:"mock_http"` +} + +// 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 *ocm.ShareId `json:"id"` + ResourceID *provider.ResourceId `json:"resource_id"` + Permissions *ocm.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 ocm.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 a 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) +} + +// NewShareManager returns a new Nextcloud-based ShareManager +func NewShareManager(c *ShareManagerConfig) (*Manager, error) { + var client *http.Client + if c.MockHTTP { + // called := make([]string, 0) + // nextcloudServerMock := GetNextcloudServerMock(&called) + // client, _ = TestingHTTPClient(nextcloudServerMock) + + // Wait for SetHTTPClient to be called later + client = nil + } else { + client = &http.Client{} + } + + return &Manager{ + endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" + client: client, + }, nil +} + +// SetHTTPClient sets the HTTP client +func (sm *Manager) SetHTTPClient(c *http.Client) { + sm.client = c +} + +func (sm *Manager) 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 + url := sm.endPoint + "~" + user.Username + "/api/ocm/" + 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 +} + +// Share as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string, + pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) { + type paramsObj struct { + Md *provider.ResourceId `json:"md"` + G *ocm.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 &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.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 &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) Unshare(ctx context.Context, ref *ocm.ShareReference) error { + bodyStr, err := json.Marshal(ref) + if err != nil { + return err + } + + _, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}) + return err +} + +// UpdateShare as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) { + type paramsObj struct { + Ref *ocm.ShareReference `json:"ref"` + P *ocm.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 &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.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([]*ocm.Share, len(respArr)) + for i := 0; i < len(respArr); i++ { + altResult := respArr[i] + pointers[i] = &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm *Manager) ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) { + _, respBody, err := sm.do(ctx, Action{"ListReceivedShares", string("")}) + if err != nil { + return nil, err + } + + var respArr []ReceivedShareAltMap + err = json.Unmarshal(respBody, &respArr) + if err != nil { + return nil, err + } + var pointers = make([]*ocm.ReceivedShare, len(respArr)) + for i := 0; i < len(respArr); i++ { + altResultShare := respArr[i].Share + if altResultShare == nil { + pointers[i] = &ocm.ReceivedShare{ + Share: nil, + State: respArr[i].State, + } + } else { + pointers[i] = &ocm.ReceivedShare{ + Share: &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L29-L54 +func (sm *Manager) GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.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 &ocm.ReceivedShare{ + Share: nil, + State: altResult.State, + }, err + } + return &ocm.ReceivedShare{ + Share: &ocm.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 as defined in the ocm.share.Manager interface +// https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57 +func (sm Manager) UpdateReceivedShare(ctx context.Context, receivedShare *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) { + type paramsObj struct { + ReceivedShare *ocm.ReceivedShare `json:"received_share"` + FieldMask *field_mask.FieldMask `json:"field_mask"` + } + + bodyObj := ¶msObj{ + ReceivedShare: receivedShare, + FieldMask: fieldMask, + } + 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 &ocm.ReceivedShare{ + Share: nil, + State: altResult.State, + }, err + } + return &ocm.ReceivedShare{ + Share: &ocm.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/ocm/share/manager/nextcloud/nextcloud_server_mock.go b/pkg/ocm/share/manager/nextcloud/nextcloud_server_mock.go new file mode 100644 index 0000000000..738bef43f6 --- /dev/null +++ b/pkg/ocm/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/ocm/Share {"md":{"opaque_id":"fileid-/some/path"},"g":{"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"permissions":{"permissions":{"get_path":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/ocm/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/ocm/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`: {200, ``, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/ocm/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/ocm/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/ocm/ListReceivedShares `: {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/ocm/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/ocm/UpdateReceivedShare {"received_share":{"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},"field_mask":{"paths":["state"]}}`: {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/ocm/share/manager/nextcloud/nextcloud_suite_test.go b/pkg/ocm/share/manager/nextcloud/nextcloud_suite_test.go new file mode 100644 index 0000000000..7d75b64879 --- /dev/null +++ b/pkg/ocm/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/ocm/share/manager/nextcloud/nextcloud_test.go b/pkg/ocm/share/manager/nextcloud/nextcloud_test.go new file mode 100644 index 0000000000..c3469d6305 --- /dev/null +++ b/pkg/ocm/share/manager/nextcloud/nextcloud_test.go @@ -0,0 +1,832 @@ +// 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" + "fmt" + "os" + + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/metadata" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/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/ocm/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" +) + +func setUpNextcloudServer() (*nextcloud.Manager, *[]string, func()) { + var conf *nextcloud.ShareManagerConfig + + ncHost := os.Getenv("NEXTCLOUD") + fmt.Printf(`NEXTCLOUD env var: "%s"`, ncHost) + if len(ncHost) == 0 { + conf = &nextcloud.ShareManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + MockHTTP: true, + } + nc, _ := nextcloud.NewShareManager(conf) + called := make([]string, 0) + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + nc.SetHTTPClient(mock) + return nc, &called, teardown + } + conf = &nextcloud.ShareManagerConfig{ + EndPoint: ncHost + "/apps/sciencemesh/", + MockHTTP: false, + } + nc, _ := nextcloud.NewShareManager(conf) + return nc, nil, func() {} +} + +func checkCalled(called *[]string, expected string) { + if called == nil { + return + } + Expect(len(*called)).To(Equal(1)) + Expect((*called)[0]).To(Equal(expected)) +} + +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 *ocm.ShareGrant) (*ocm.Share, error) + Describe("Share", func() { + It("calls the Share endpoint", func() { + fmt.Println("Calling setUpNextCloudServer!") + am, called, teardown := setUpNextcloudServer() + defer teardown() + var md = &provider.ResourceId{ + StorageId: "", + OpaqueId: "fileid-/some/path", + } + var g = &ocm.ShareGrant{ + 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, + }, + }, + }, + Permissions: &ocm.SharePermissions{ + Permissions: &provider.ResourcePermissions{ + AddGrant: false, + CreateContainer: false, + Delete: false, + GetPath: true, + 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, + }, + }, + } + var name = "Some Name" + var pi = &ocmprovider.ProviderInfo{} + var pm = "some-permissions-string?" + var owner = &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + } + var token = "some-token" + var st = ocm.Share_SHARE_TYPE_REGULAR + share, err := am.Share(ctx, md, g, name, pi, pm, owner, token, st) + + Expect(err).ToNot(HaveOccurred()) + Expect(*share).To(Equal(ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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, + }, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/Share {"md":{"opaque_id":"fileid-/some/path"},"g":{"grantee":{"Id":{"UserId":{"idp":"0.0.0.0:19000","opaque_id":"f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c","type":1}}},"permissions":{"permissions":{"get_path":true}}}}`) + }) + }) + + // GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) + Describe("GetShare", func() { + It("calls the GetShare endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + share, err := am.GetShare(ctx, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*share).To(Equal(ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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, + }, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/GetShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`) + }) + }) + + // Unshare(ctx context.Context, ref *ocm.ShareReference) error + Describe("Unshare", func() { + It("calls the Unshare endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + err := am.Unshare(ctx, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/Unshare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`) + }) + }) + + // UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) + Describe("UpdateShare", func() { + It("calls the UpdateShare endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + share, err := am.UpdateShare(ctx, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }, + &ocm.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(ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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, + }, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/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 []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) + Describe("ListShares", func() { + It("calls the ListShares endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + shares, err := am.ListShares(ctx, []*ocm.ListOCMSharesRequest_Filter{ + { + Type: ocm.ListOCMSharesRequest_Filter_TYPE_CREATOR, + Term: &ocm.ListOCMSharesRequest_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(ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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, + }, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/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 []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.ReceivedShare, error) + Describe("ListReceivedShares", func() { + It("calls the ListReceivedShares endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + receivedShares, err := am.ListReceivedShares(ctx) + Expect(err).ToNot(HaveOccurred()) + Expect(len(receivedShares)).To(Equal(1)) + Expect(*receivedShares[0]).To(Equal(ocm.ReceivedShare{ + Share: &ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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: ocm.ShareState_SHARE_STATE_ACCEPTED, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/ListReceivedShares `) + }) + }) + + // GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) + Describe("GetReceivedShare", func() { + It("calls the GetReceivedShare endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + receivedShare, err := am.GetReceivedShare(ctx, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: "some-share-id", + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*receivedShare).To(Equal(ocm.ReceivedShare{ + Share: &ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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: ocm.ShareState_SHARE_STATE_ACCEPTED, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/GetReceivedShare {"Spec":{"Id":{"opaque_id":"some-share-id"}}}`) + }) + }) + + // UpdateReceivedShare(ctx context.Context, receivedShare *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) + Describe("UpdateReceivedShare", func() { + It("calls the UpdateReceivedShare endpoint", func() { + am, called, teardown := setUpNextcloudServer() + defer teardown() + + receivedShare, err := am.UpdateReceivedShare(ctx, + &ocm.ReceivedShare{ + Share: &ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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: ocm.ShareState_SHARE_STATE_ACCEPTED, + }, + &field_mask.FieldMask{ + Paths: []string{"state"}, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(*receivedShare).To(Equal(ocm.ReceivedShare{ + Share: &ocm.Share{ + Id: &ocm.ShareId{}, + ResourceId: &provider.ResourceId{}, + Permissions: &ocm.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: ocm.ShareState_SHARE_STATE_ACCEPTED, + })) + checkCalled(called, `POST /apps/sciencemesh/~tester/api/ocm/UpdateReceivedShare {"received_share":{"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},"field_mask":{"paths":["state"]}}`) + }) + }) + +})