github.com/cs3org/reva/v2@v2.27.7/pkg/ocm/share/repository/nextcloud/nextcloud.go (about)

     1  // Copyright 2018-2023 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  // Package nextcloud verifies a clientID and clientSecret against a Nextcloud backend.
    20  package nextcloud
    21  
    22  import (
    23  	"context"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io"
    27  	"net/http"
    28  	"strings"
    29  
    30  	userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
    31  	"github.com/cs3org/reva/v2/pkg/errtypes"
    32  
    33  	ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
    34  	provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
    35  	typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    36  	"github.com/cs3org/reva/v2/pkg/appctx"
    37  	"github.com/cs3org/reva/v2/pkg/ocm/share"
    38  	"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
    39  	"github.com/cs3org/reva/v2/pkg/utils"
    40  	"github.com/cs3org/reva/v2/pkg/utils/cfg"
    41  	"github.com/pkg/errors"
    42  	"google.golang.org/genproto/protobuf/field_mask"
    43  )
    44  
    45  func init() {
    46  	registry.Register("nextcloud", New)
    47  }
    48  
    49  // Manager is the Nextcloud-based implementation of the share.Repository interface
    50  // see https://github.com/cs3org/reva/blob/v1.13.0/pkg/ocm/share/share.go#L30-L57
    51  type Manager struct {
    52  	client       *http.Client
    53  	sharedSecret string
    54  	webDAVHost   string
    55  	endPoint     string
    56  }
    57  
    58  // ShareManagerConfig contains config for a Nextcloud-based ShareManager.
    59  type ShareManagerConfig struct {
    60  	EndPoint     string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"`
    61  	SharedSecret string `mapstructure:"shared_secret"`
    62  	WebDAVHost   string `mapstructure:"webdav_host"`
    63  	MockHTTP     bool   `mapstructure:"mock_http"`
    64  }
    65  
    66  // Action describes a REST request to forward to the Nextcloud backend.
    67  type Action struct {
    68  	verb string
    69  	argS string
    70  }
    71  
    72  // GranteeAltMap is an alternative map to JSON-unmarshal a Grantee
    73  // Grantees are hard to unmarshal, so unmarshalling into a map[string]interface{} first,
    74  // see also https://github.com/pondersource/sciencemesh-nextcloud/issues/27
    75  type GranteeAltMap struct {
    76  	ID *provider.Grantee_UserId `json:"id"`
    77  }
    78  
    79  // ShareAltMap is an alternative map to JSON-unmarshal a Share.
    80  type ShareAltMap struct {
    81  	ID            *ocm.ShareId          `json:"id"`
    82  	RemoteShareID string                `json:"remote_share_id"`
    83  	Permissions   *ocm.SharePermissions `json:"permissions"`
    84  	Grantee       *GranteeAltMap        `json:"grantee"`
    85  	Owner         *userpb.UserId        `json:"owner"`
    86  	Creator       *userpb.UserId        `json:"creator"`
    87  	Ctime         *typespb.Timestamp    `json:"ctime"`
    88  	Mtime         *typespb.Timestamp    `json:"mtime"`
    89  }
    90  
    91  // ReceivedShareAltMap is an alternative map to JSON-unmarshal a ReceivedShare.
    92  type ReceivedShareAltMap struct {
    93  	Share *ShareAltMap   `json:"share"`
    94  	State ocm.ShareState `json:"state"`
    95  }
    96  
    97  // New returns a share manager implementation that verifies against a Nextcloud backend.
    98  func New(m map[string]interface{}) (share.Repository, error) {
    99  	var c ShareManagerConfig
   100  	if err := cfg.Decode(m, &c); err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	return NewShareManager(&c)
   105  }
   106  
   107  // NewShareManager returns a new Nextcloud-based ShareManager.
   108  func NewShareManager(c *ShareManagerConfig) (*Manager, error) {
   109  	var client *http.Client
   110  	if c.MockHTTP {
   111  		// called := make([]string, 0)
   112  		// nextcloudServerMock := GetNextcloudServerMock(&called)
   113  		// client, _ = TestingHTTPClient(nextcloudServerMock)
   114  
   115  		// Wait for SetHTTPClient to be called later
   116  		client = nil
   117  	} else {
   118  		if len(c.EndPoint) == 0 {
   119  			return nil, errors.New("Please specify 'endpoint' in '[grpc.services.ocmshareprovider.drivers.nextcloud]' and  '[grpc.services.ocmcore.drivers.nextcloud]'")
   120  		}
   121  		client = &http.Client{}
   122  	}
   123  
   124  	return &Manager{
   125  		endPoint:     c.EndPoint, // e.g. "http://nc/apps/sciencemesh/"
   126  		sharedSecret: c.SharedSecret,
   127  		client:       client,
   128  		webDAVHost:   c.WebDAVHost,
   129  	}, nil
   130  }
   131  
   132  // SetHTTPClient sets the HTTP client.
   133  func (sm *Manager) SetHTTPClient(c *http.Client) {
   134  	sm.client = c
   135  }
   136  
   137  // StoreShare stores a share.
   138  func (sm *Manager) StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) {
   139  	encShare, err := utils.MarshalProtoV1ToJSON(share)
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  	_, body, err := sm.do(ctx, Action{"addSentShare", string(encShare)}, getUsername(&userpb.User{Id: share.Creator}))
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	share.Id = &ocm.ShareId{
   148  		OpaqueId: string(body),
   149  	}
   150  	return share, nil
   151  }
   152  
   153  // GetShare gets the information for a share by the given ref.
   154  func (sm *Manager) GetShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.Share, error) {
   155  	data, err := json.Marshal(ref)
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  	_, body, err := sm.do(ctx, Action{"GetShare", string(data)}, getUsername(user))
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	altResult := &ShareAltMap{}
   165  	if err := json.Unmarshal(body, &altResult); err != nil {
   166  		return nil, err
   167  	}
   168  	return &ocm.Share{
   169  		Id: altResult.ID,
   170  		Grantee: &provider.Grantee{
   171  			Id: altResult.Grantee.ID,
   172  		},
   173  		Owner:   altResult.Owner,
   174  		Creator: altResult.Creator,
   175  		Ctime:   altResult.Ctime,
   176  		Mtime:   altResult.Mtime,
   177  	}, nil
   178  }
   179  
   180  // DeleteShare deletes the share pointed by ref.
   181  func (sm *Manager) DeleteShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
   182  	bodyStr, err := json.Marshal(ref)
   183  	if err != nil {
   184  		return err
   185  	}
   186  
   187  	_, _, err = sm.do(ctx, Action{"Unshare", string(bodyStr)}, getUsername(user))
   188  	return err
   189  }
   190  
   191  // UpdateShare updates the mode of the given share.
   192  func (sm *Manager) UpdateShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference, f ...*ocm.UpdateOCMShareRequest_UpdateField) (*ocm.Share, error) {
   193  	type paramsObj struct {
   194  		Ref *ocm.ShareReference   `json:"ref"`
   195  		P   *ocm.SharePermissions `json:"p"`
   196  	}
   197  	bodyObj := &paramsObj{
   198  		Ref: ref,
   199  	}
   200  	data, err := json.Marshal(bodyObj)
   201  	if err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	_, body, err := sm.do(ctx, Action{"UpdateShare", string(data)}, getUsername(user))
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	altResult := &ShareAltMap{}
   211  	if err := json.Unmarshal(body, &altResult); err != nil {
   212  		return nil, err
   213  	}
   214  	return &ocm.Share{
   215  		Id: altResult.ID,
   216  		Grantee: &provider.Grantee{
   217  			Id: altResult.Grantee.ID,
   218  		},
   219  		Owner:   altResult.Owner,
   220  		Creator: altResult.Creator,
   221  		Ctime:   altResult.Ctime,
   222  		Mtime:   altResult.Mtime,
   223  	}, nil
   224  }
   225  
   226  // ListShares returns the shares created by the user. If md is provided is not nil,
   227  // it returns only shares attached to the given resource.
   228  func (sm *Manager) ListShares(ctx context.Context, user *userpb.User, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) {
   229  	data, err := json.Marshal(filters)
   230  	if err != nil {
   231  		return nil, err
   232  	}
   233  
   234  	_, respBody, err := sm.do(ctx, Action{"ListShares", string(data)}, getUsername(user))
   235  	if err != nil {
   236  		return nil, err
   237  	}
   238  
   239  	var respArr []ShareAltMap
   240  	if err := json.Unmarshal(respBody, &respArr); err != nil {
   241  		return nil, err
   242  	}
   243  
   244  	var lst = make([]*ocm.Share, 0, len(respArr))
   245  	for _, altResult := range respArr {
   246  		lst = append(lst, &ocm.Share{
   247  			Id: altResult.ID,
   248  			Grantee: &provider.Grantee{
   249  				Id: altResult.Grantee.ID,
   250  			},
   251  			Owner:   altResult.Owner,
   252  			Creator: altResult.Creator,
   253  			Ctime:   altResult.Ctime,
   254  			Mtime:   altResult.Mtime,
   255  		})
   256  	}
   257  	return lst, nil
   258  }
   259  
   260  // StoreReceivedShare stores a received share.
   261  func (sm *Manager) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) {
   262  	data, err := utils.MarshalProtoV1ToJSON(share)
   263  	if err != nil {
   264  		return nil, err
   265  	}
   266  	_, body, err := sm.do(ctx, Action{"addReceivedShare", string(data)}, getUsername(&userpb.User{Id: share.Grantee.GetUserId()}))
   267  	if err != nil {
   268  		return nil, err
   269  	}
   270  	share.Id = &ocm.ShareId{
   271  		OpaqueId: string(body),
   272  	}
   273  
   274  	return share, nil
   275  }
   276  
   277  // ListReceivedShares returns the list of shares the user has access.
   278  func (sm *Manager) ListReceivedShares(ctx context.Context, user *userpb.User) ([]*ocm.ReceivedShare, error) {
   279  	log := appctx.GetLogger(ctx)
   280  	_, respBody, err := sm.do(ctx, Action{"ListReceivedShares", ""}, getUsername(user))
   281  	if err != nil {
   282  		return nil, err
   283  	}
   284  
   285  	var respArr []ReceivedShareAltMap
   286  	if err := json.Unmarshal(respBody, &respArr); err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	res := make([]*ocm.ReceivedShare, 0, len(respArr))
   291  	for _, share := range respArr {
   292  		altResultShare := share.Share
   293  		log.Info().Msgf("Unpacking share object %+v\n", altResultShare)
   294  		if altResultShare == nil {
   295  			continue
   296  		}
   297  		res = append(res, &ocm.ReceivedShare{
   298  			Id:            altResultShare.ID,
   299  			RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
   300  			Grantee: &provider.Grantee{
   301  				Id: altResultShare.Grantee.ID,
   302  			},
   303  			Owner:   altResultShare.Owner,
   304  			Creator: altResultShare.Creator,
   305  			Ctime:   altResultShare.Ctime,
   306  			Mtime:   altResultShare.Mtime,
   307  			State:   share.State,
   308  		})
   309  	}
   310  	return res, nil
   311  }
   312  
   313  // GetReceivedShare returns the information for a received share the user has access.
   314  func (sm *Manager) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) {
   315  	data, err := json.Marshal(ref)
   316  	if err != nil {
   317  		return nil, err
   318  	}
   319  
   320  	_, respBody, err := sm.do(ctx, Action{"GetReceivedShare", string(data)}, getUsername(user))
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  
   325  	var altResult ReceivedShareAltMap
   326  	if err := json.Unmarshal(respBody, &altResult); err != nil {
   327  		return nil, err
   328  	}
   329  	altResultShare := altResult.Share
   330  	if altResultShare == nil {
   331  		return &ocm.ReceivedShare{
   332  			State: altResult.State,
   333  		}, nil
   334  	}
   335  	return &ocm.ReceivedShare{
   336  		Id:            altResultShare.ID,
   337  		RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
   338  		Grantee: &provider.Grantee{
   339  			Id: altResultShare.Grantee.ID,
   340  		},
   341  		Owner:   altResultShare.Owner,
   342  		Creator: altResultShare.Creator,
   343  		Ctime:   altResultShare.Ctime,
   344  		Mtime:   altResultShare.Mtime,
   345  		State:   altResult.State,
   346  	}, nil
   347  }
   348  
   349  // DeleteReceivedShare deletes the share pointed by ref.
   350  func (sm *Manager) DeleteReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
   351  	return errtypes.NotSupported("not implemented")
   352  }
   353  
   354  // UpdateReceivedShare updates the received share with share state.
   355  func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
   356  	type paramsObj struct {
   357  		ReceivedShare *ocm.ReceivedShare    `json:"received_share"`
   358  		FieldMask     *field_mask.FieldMask `json:"field_mask"`
   359  	}
   360  
   361  	bodyObj := &paramsObj{
   362  		ReceivedShare: share,
   363  		FieldMask:     fieldMask,
   364  	}
   365  	bodyStr, err := json.Marshal(bodyObj)
   366  	if err != nil {
   367  		return nil, err
   368  	}
   369  
   370  	_, respBody, err := sm.do(ctx, Action{"UpdateReceivedShare", string(bodyStr)}, getUsername(user))
   371  	if err != nil {
   372  		return nil, err
   373  	}
   374  
   375  	var altResult ReceivedShareAltMap
   376  	err = json.Unmarshal(respBody, &altResult)
   377  	if err != nil {
   378  		return nil, err
   379  	}
   380  	altResultShare := altResult.Share
   381  	if altResultShare == nil {
   382  		return &ocm.ReceivedShare{
   383  			State: altResult.State,
   384  		}, nil
   385  	}
   386  	return &ocm.ReceivedShare{
   387  		Id:            altResultShare.ID,
   388  		RemoteShareId: altResultShare.RemoteShareID, // sic, see https://github.com/cs3org/reva/pull/3852#discussion_r1189681465
   389  		Grantee: &provider.Grantee{
   390  			Id: altResultShare.Grantee.ID,
   391  		},
   392  		Owner:   altResultShare.Owner,
   393  		Creator: altResultShare.Creator,
   394  		Ctime:   altResultShare.Ctime,
   395  		Mtime:   altResultShare.Mtime,
   396  		State:   altResult.State,
   397  	}, nil
   398  }
   399  
   400  func getUsername(user *userpb.User) string {
   401  	if user != nil && len(user.Username) > 0 {
   402  		return user.Username
   403  	}
   404  	if user != nil && len(user.Id.OpaqueId) > 0 {
   405  		return user.Id.OpaqueId
   406  	}
   407  
   408  	return "empty-username"
   409  }
   410  
   411  func (sm *Manager) do(ctx context.Context, a Action, username string) (int, []byte, error) {
   412  	url := sm.endPoint + "~" + username + "/api/ocm/" + a.verb
   413  
   414  	log := appctx.GetLogger(ctx)
   415  	log.Info().Msgf("am.do %s %s", url, a.argS)
   416  	req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS))
   417  	if err != nil {
   418  		return 0, nil, err
   419  	}
   420  	req.Header.Set("X-Reva-Secret", sm.sharedSecret)
   421  
   422  	req.Header.Set("Content-Type", "application/json")
   423  	resp, err := sm.client.Do(req)
   424  	if err != nil {
   425  		return 0, nil, err
   426  	}
   427  
   428  	defer resp.Body.Close()
   429  	body, err := io.ReadAll(resp.Body)
   430  	if err != nil {
   431  		return 0, nil, err
   432  	}
   433  
   434  	// curl -i -H 'application/json' -H 'X-Reva-Secret: shared-secret-1' -d '{"md":{"opaque_id":"fileid-/other/q/as"},"g":{"grantee":{"type":1,"Id":{"UserId":{"idp":"revanc2.docker","opaque_id":"marie"}}},"permissions":{"permissions":{"get_path":true,"initiate_file_download":true,"list_container":true,"list_file_versions":true,"stat":true}}},"provider_domain":"cern.ch","resource_type":"file","provider_id":2,"owner_opaque_id":"einstein","owner_display_name":"Albert Einstein","protocol":{"name":"webdav","options":{"sharedSecret":"secret","permissions":"webdav-property"}}}' https://nc1.docker/index.php/apps/sciencemesh/~/api/ocm/addSentShare
   435  
   436  	log.Info().Msgf("am.do response %d %s", resp.StatusCode, body)
   437  
   438  	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
   439  		return 0, nil, fmt.Errorf("Unexpected response code from EFSS API: %d", resp.StatusCode)
   440  	}
   441  	return resp.StatusCode, body, nil
   442  }