github.com/cs3org/reva/v2@v2.27.7/pkg/ocm/client/client.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 client
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"net/url"
    28  	"strconv"
    29  	"time"
    30  
    31  	"github.com/cs3org/reva/v2/pkg/appctx"
    32  	"github.com/cs3org/reva/v2/pkg/errtypes"
    33  	"github.com/cs3org/reva/v2/pkg/rhttp"
    34  	"github.com/pkg/errors"
    35  )
    36  
    37  // ErrTokenInvalid is the error returned by the invite-accepted
    38  // endpoint when the token is not valid.
    39  var ErrTokenInvalid = errors.New("the invitation token is invalid")
    40  
    41  // ErrServiceNotTrusted is the error returned by the invite-accepted
    42  // endpoint when the service is not trusted to accept invitations.
    43  var ErrServiceNotTrusted = errors.New("service is not trusted to accept invitations")
    44  
    45  // ErrUserAlreadyAccepted is the error returned by the invite-accepted
    46  // endpoint when a user is already know by the remote cloud.
    47  var ErrUserAlreadyAccepted = errors.New("user already accepted an invitation token")
    48  
    49  // ErrTokenNotFound is the error returned by the invite-accepted
    50  // endpoint when the request is done using a not existing token.
    51  var ErrTokenNotFound = errors.New("token not found")
    52  
    53  // ErrInvalidParameters is the error returned by the shares endpoint
    54  // when the request does not contain required properties.
    55  var ErrInvalidParameters = errors.New("invalid parameters")
    56  
    57  // OCMClient is the client for an OCM provider.
    58  type OCMClient struct {
    59  	client *http.Client
    60  }
    61  
    62  // Config is the configuration to be used for the OCMClient.
    63  type Config struct {
    64  	Timeout  time.Duration
    65  	Insecure bool
    66  }
    67  
    68  // New returns a new OCMClient.
    69  func New(c *Config) *OCMClient {
    70  	return &OCMClient{
    71  		client: rhttp.GetHTTPClient(
    72  			rhttp.Timeout(c.Timeout),
    73  			rhttp.Insecure(c.Insecure),
    74  		),
    75  	}
    76  }
    77  
    78  // InviteAccepted informs the sender that the invitation was accepted to start sharing
    79  // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
    80  func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *InviteAcceptedRequest) (*User, error) {
    81  	url, err := url.JoinPath(endpoint, "invite-accepted")
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  
    86  	body, err := r.toJSON()
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  
    91  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
    92  	if err != nil {
    93  		return nil, errors.Wrap(err, "error creating request")
    94  	}
    95  	req.Header.Set("Content-Type", "application/json")
    96  
    97  	resp, err := c.client.Do(req)
    98  	if err != nil {
    99  		return nil, errors.Wrap(err, "error doing request")
   100  	}
   101  	defer resp.Body.Close()
   102  
   103  	return c.parseInviteAcceptedResponse(resp)
   104  }
   105  
   106  func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) {
   107  	switch r.StatusCode {
   108  	case http.StatusOK:
   109  		var u User
   110  		if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
   111  			return nil, errors.Wrap(err, "error decoding response body")
   112  		}
   113  		return &u, nil
   114  	case http.StatusBadRequest:
   115  		return nil, ErrTokenInvalid
   116  	case http.StatusNotFound:
   117  		return nil, ErrTokenNotFound
   118  	case http.StatusConflict:
   119  		return nil, ErrUserAlreadyAccepted
   120  	case http.StatusForbidden:
   121  		return nil, ErrServiceNotTrusted
   122  	}
   123  
   124  	body, err := io.ReadAll(r.Body)
   125  	if err != nil {
   126  		return nil, errors.Wrap(err, "error decoding response body")
   127  	}
   128  	return nil, errtypes.InternalError(string(body))
   129  }
   130  
   131  // NewShare creates a new share.
   132  // https://github.com/cs3org/OCM-API/blob/develop/spec.yaml
   133  func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRequest) (*NewShareResponse, error) {
   134  	url, err := url.JoinPath(endpoint, "shares")
   135  	if err != nil {
   136  		return nil, err
   137  	}
   138  
   139  	body, err := r.toJSON()
   140  	if err != nil {
   141  		return nil, err
   142  	}
   143  
   144  	log := appctx.GetLogger(ctx)
   145  	log.Debug().Msgf("Sending OCM /shares POST to %s: %s", url, body)
   146  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
   147  	if err != nil {
   148  		return nil, errors.Wrap(err, "error creating request")
   149  	}
   150  	req.Header.Set("Content-Type", "application/json")
   151  
   152  	resp, err := c.client.Do(req)
   153  	if err != nil {
   154  		return nil, errors.Wrap(err, "error doing request")
   155  	}
   156  	defer resp.Body.Close()
   157  
   158  	return c.parseNewShareResponse(resp)
   159  }
   160  
   161  func (c *OCMClient) parseNewShareResponse(r *http.Response) (*NewShareResponse, error) {
   162  	switch r.StatusCode {
   163  	case http.StatusOK, http.StatusCreated:
   164  		var res NewShareResponse
   165  		err := json.NewDecoder(r.Body).Decode(&res)
   166  		return &res, err
   167  	case http.StatusBadRequest:
   168  		return nil, ErrInvalidParameters
   169  	case http.StatusUnauthorized, http.StatusForbidden:
   170  		return nil, ErrServiceNotTrusted
   171  	}
   172  
   173  	body, err := io.ReadAll(r.Body)
   174  	if err != nil {
   175  		return nil, errors.Wrap(err, "error decoding response body")
   176  	}
   177  	return nil, errtypes.InternalError(string(body))
   178  }
   179  
   180  // Discovery returns a number of properties used to discover the capabilities offered by a remote cloud storage.
   181  // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get
   182  func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabilities, error) {
   183  	url, err := url.JoinPath(endpoint, "shares")
   184  	if err != nil {
   185  		return nil, err
   186  	}
   187  
   188  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
   189  	if err != nil {
   190  		return nil, errors.Wrap(err, "error creating request")
   191  	}
   192  	req.Header.Set("Content-Type", "application/json")
   193  
   194  	resp, err := c.client.Do(req)
   195  	if err != nil {
   196  		return nil, errors.Wrap(err, "error doing request")
   197  	}
   198  	defer resp.Body.Close()
   199  
   200  	var cap Capabilities
   201  	if err := json.NewDecoder(resp.Body).Decode(&c); err != nil {
   202  		return nil, err
   203  	}
   204  
   205  	return &cap, nil
   206  }
   207  
   208  // NotifyRemote sends a notification to a remote OCM instance.
   209  // Send a notification to a remote party about a previously known entity
   210  // Notifications are optional messages. They are expected to be used to inform the other party about a change about a previously known entity,
   211  // such as a share or a trusted user. For example, a notification MAY be sent by a recipient to let the provider know that
   212  // the recipient declined a share. In this case, the provider site MAY mark the share as declined for its user(s). Similarly,
   213  // it MAY be sent by a provider to let the recipient know that the provider removed a given share, such that the recipient MAY clean it up from its database.
   214  // A notification MAY also be sent to let a recipient know that the provider removed that recipient from the list of trusted users, along with any related share.
   215  // The recipient MAY reciprocally remove that provider from the list of trusted users, along with any related share.
   216  // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
   217  func (c *OCMClient) NotifyRemote(ctx context.Context, endpoint string, r *NotificationRequest) error {
   218  	url, err := url.JoinPath(endpoint, "notifications")
   219  	if err != nil {
   220  		return err
   221  	}
   222  	body, err := r.ToJSON()
   223  	if err != nil {
   224  		return err
   225  	}
   226  	req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
   227  	if err != nil {
   228  		return errors.Wrap(err, "error creating request")
   229  	}
   230  	req.Header.Set("Content-Type", "application/json")
   231  
   232  	resp, err := c.client.Do(req)
   233  	if err != nil {
   234  		return errors.Wrap(err, "error doing request")
   235  	}
   236  	defer resp.Body.Close()
   237  
   238  	err = c.parseNotifyRemoteResponse(resp, nil)
   239  	if err != nil {
   240  		appctx.GetLogger(ctx).Err(err).Msg("error notifying remote OCM instance")
   241  		return err
   242  	}
   243  	return nil
   244  }
   245  
   246  func (c *OCMClient) parseNotifyRemoteResponse(r *http.Response, resp any) error {
   247  	var err error
   248  	switch r.StatusCode {
   249  	case http.StatusOK, http.StatusCreated:
   250  		if resp == nil {
   251  			return nil
   252  		}
   253  		err := json.NewDecoder(r.Body).Decode(resp)
   254  		if err != nil {
   255  			return errors.Wrap(err, fmt.Sprintf("http status code: %v, error decoding response body", r.StatusCode))
   256  		}
   257  		return nil
   258  	case http.StatusBadRequest:
   259  		err = ErrInvalidParameters
   260  	case http.StatusUnauthorized, http.StatusForbidden:
   261  		err = ErrServiceNotTrusted
   262  	default:
   263  		err = errtypes.InternalError("request finished whit code " + strconv.Itoa(r.StatusCode))
   264  	}
   265  
   266  	body, err2 := io.ReadAll(r.Body)
   267  	if err2 != nil {
   268  		return errors.Wrap(err, "error reading response body "+err2.Error())
   269  	}
   270  	return errors.Wrap(err, string(body))
   271  }