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 := ¶msObj{ 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 := ¶msObj{ 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 }