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 }