Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic implementation of OCM 1.2 payloads #5076

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
11 changes: 11 additions & 0 deletions changelog/unreleased/ocm1.2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Enhancement: implement OCM 1.2

This PR brings in the implementation of parts of OpenCloudMesh 1.2, including:
* Adopting the new properties of the OCM 1.2 payloads, without implementing any new functionality for now. In particular, any non-empty `requirement` in a share will be rejected (a test was added for that).
* Extending the OCM discovery endpoint.
* Using the remote OCM discovery endpoint to establish the full URL of an incoming remote share, regardless if provided or not. When sending a share, though, we still send a full URL.
* Caching the webdav client used to connect to remote endpoints, with added compatibility to OCM 1.0 remote servers.
* Some refactoring and consolidation of duplicated code.
* Improved logging.

https://github.com/cs3org/reva/pull/5076
2 changes: 1 addition & 1 deletion cmd/reva/ocm-share-create.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ func getAccessMethods(webdav, webapp, datatx bool, rol string) ([]*ocm.AccessMet
if err != nil {
return nil, err
}
m = append(m, ocmshare.NewWebDavAccessMethod(perm))
m = append(m, ocmshare.NewWebDavAccessMethod(perm, []string{}))
}
if webapp {
v, err := getOCMViewMode(rol)
Expand Down
3 changes: 2 additions & 1 deletion cmd/reva/ocm-share-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ func ocmShareUpdateCommand() *command {
AccessMethods: &ocm.AccessMethod{
Term: &ocm.AccessMethod_WebdavOptions{
WebdavOptions: &ocm.WebDAVAccessMethod{
Permissions: perm,
Permissions: perm,
Requirements: []string{},
},
},
},
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/coreos/go-oidc/v3 v3.12.0
github.com/creasty/defaults v1.8.0
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1
github.com/cs3org/go-cs3apis v0.0.0-20250218144737-544dd3919658
github.com/dgraph-io/ristretto v0.2.0
github.com/dolthub/go-mysql-server v0.14.0
github.com/gdexlab/go-render v1.0.1
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -891,8 +891,8 @@ github.com/creasty/defaults v1.8.0 h1:z27FJxCAa0JKt3utc0sCImAEb+spPucmKoOdLHvHYK
github.com/creasty/defaults v1.8.0/go.mod h1:iGzKe6pbEHnpMPtfDXZEr0NVxWnPTjb1bbDy08fPzYM=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8=
github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4=
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1 h1:RU6LT6mkD16xZs011+8foU7T3LrPvTTSWeTQ9OgfhkA=
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
github.com/cs3org/go-cs3apis v0.0.0-20250218144737-544dd3919658 h1:CmH7twDuNUrHQXChZMafWjsEp1V47KutJlOAt6FjzGA=
github.com/cs3org/go-cs3apis v0.0.0-20250218144737-544dd3919658/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand Down
5 changes: 3 additions & 2 deletions internal/grpc/services/ocmshareprovider/ocmshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,8 @@ func (s *service) getWebdavProtocol(share *ocm.Share, m *ocm.AccessMethod_Webdav

return &ocmd.WebDAV{
Permissions: perms,
URL: s.webdavURL(share),
Requirements: m.WebdavOptions.Requirements,
URI: s.webdavURL(share),
SharedSecret: share.Token,
}
}
Expand All @@ -202,7 +203,7 @@ func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp {
panic(err)
}
return &ocmd.Webapp{
URITemplate: b.String(),
URI: b.String(),
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/http/services/experimental/sciencemesh/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (st
return "", errtypes.BadRequest("share does not contain webapp protocol")
}

return webapp.UriTemplate, nil
return webapp.Uri, nil
}

func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) {
Expand Down
6 changes: 3 additions & 3 deletions internal/http/services/experimental/sciencemesh/share.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {

perm, viewMode := getPermissionsByRole(req.Role)

log.Debug().Msg("calling gatewayClient.CreateOCMShare from sciencemesh/share.go")
log.Debug().Msg("calling gatewayClient.CreateOCMShare")
shareRes, err := h.gatewayClient.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{
ResourceId: statRes.Info.Id,
Grantee: &providerpb.Grantee{
Expand All @@ -117,11 +117,11 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
},
RecipientMeshProvider: recipientProviderInfo.ProviderInfo,
AccessMethods: []*ocm.AccessMethod{
share.NewWebDavAccessMethod(perm),
share.NewWebDavAccessMethod(perm, []string{}),
share.NewWebappAccessMethod(viewMode),
},
})
log.Debug().Msg("called gatewayClient.CreateOCMShare from sciencemesh/share.go")
log.Debug().Any("response", shareRes).Msg("called gatewayClient.CreateOCMShare")

switch {
case err != nil:
Expand Down
116 changes: 39 additions & 77 deletions internal/http/services/opencloudmesh/ocmd/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package ocmd

import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
Expand Down Expand Up @@ -71,70 +70,59 @@ func NewClient(timeout time.Duration, insecure bool) *OCMClient {
// Discover returns a number of properties used to discover the capabilities offered by a remote cloud storage.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get
func (c *OCMClient) Discover(ctx context.Context, endpoint string) (*wellknown.OcmDiscoveryData, error) {
url, err := url.JoinPath(endpoint, "/ocm-provider")
log := appctx.GetLogger(ctx)

remoteurl, _ := url.JoinPath(endpoint, "/.well-known/ocm")
body, err := c.discover(ctx, remoteurl)
if err != nil || len(body) == 0 {
log.Debug().Err(err).Str("sender", remoteurl).Str("response", string(body)).Msg("invalid or empty response, falling back to legacy discovery")
remoteurl, _ := url.JoinPath(endpoint, "/ocm-provider") // legacy discovery endpoint

body, err = c.discover(ctx, remoteurl)
if err != nil || len(body) == 0 {
log.Warn().Err(err).Str("sender", remoteurl).Str("response", string(body)).Msg("invalid or empty response")
return nil, errtypes.BadRequest("Invalid response on OCM discovery")
}
}

var disco wellknown.OcmDiscoveryData
err = json.Unmarshal(body, &disco)
if err != nil {
return nil, err
log.Warn().Err(err).Str("sender", remoteurl).Str("response", string(body)).Msg("malformed response")
return nil, errtypes.BadRequest("Invalid payload on OCM discovery")
}

log.Debug().Str("sender", remoteurl).Any("response", disco).Msg("discovery response")
return &disco, nil
}

func (c *OCMClient) discover(ctx context.Context, url string) ([]byte, error) {
log := appctx.GetLogger(ctx)

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, errors.Wrap(err, "error creating request")
return nil, errors.Wrap(err, "error creating OCM discovery request")
}
req.Header.Set("Content-Type", "application/json")

resp, err := c.client.Do(req)
if err != nil {
return nil, errors.Wrap(err, "error doing request")
return nil, errors.Wrap(err, "error doing OCM discovery request")
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
if resp.StatusCode != http.StatusOK {
log.Warn().Str("sender", url).Any("response", resp).Int("status", resp.StatusCode).Msg("discovery returned")
return nil, errtypes.BadRequest("Remote does not offer a valid OCM discovery endpoint")
}

var disco wellknown.OcmDiscoveryData
err = json.Unmarshal(body, &disco)
body, err := io.ReadAll(resp.Body)
if err != nil {
log := appctx.GetLogger(ctx)
log.Warn().Str("sender", endpoint).Str("response", string(body)).Msg("malformed response")
return nil, errtypes.InternalError("Invalid payload on OCM discovery")
}

return &disco, nil
}

// NewShareRequest contains the parameters for creating a new OCM share.
type NewShareRequest struct {
ShareWith string `json:"shareWith"`
Name string `json:"name"`
Description string `json:"description"`
ProviderID string `json:"providerId"`
Owner string `json:"owner"`
Sender string `json:"sender"`
OwnerDisplayName string `json:"ownerDisplayName"`
SenderDisplayName string `json:"senderDisplayName"`
ShareType string `json:"shareType"`
Expiration uint64 `json:"expiration"`
ResourceType string `json:"resourceType"`
Protocols Protocols `json:"protocol"`
}

func (r *NewShareRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
return nil, errors.Wrap(err, "malformed remote OCM discovery")
}
return &b, nil
return body, nil
}

// NewShareResponse is the response returned when creating a new share.
type NewShareResponse struct {
RecipientDisplayName string `json:"recipientDisplayName"`
}

// NewShare creates a new share.
// https://github.com/cs3org/OCM-API/blob/develop/spec.yaml
// NewShare sends a new OCM share to the remote system.
func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRequest) (*NewShareResponse, error) {
url, err := url.JoinPath(endpoint, "shares")
if err != nil {
Expand Down Expand Up @@ -182,35 +170,9 @@ func (c *OCMClient) parseNewShareResponse(r *http.Response) (*NewShareResponse,
return nil, errtypes.InternalError(string(body))
}

// InviteAcceptedRequest contains the parameters for accepting
// an invitation.
type InviteAcceptedRequest struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
RecipientProvider string `json:"recipientProvider"`
Token string `json:"token"`
}

// User contains the remote user's information when accepting
// an invitation.
type User struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}

func (r *InviteAcceptedRequest) toJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}

// InviteAccepted informs the sender that the invitation was accepted to start sharing
// InviteAccepted informs the remote end that the invitation was accepted to start sharing
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1invite-accepted/post
func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *InviteAcceptedRequest) (*User, error) {
func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *InviteAcceptedRequest) (*RemoteUser, error) {
url, err := url.JoinPath(endpoint, "invite-accepted")
if err != nil {
return nil, err
Expand All @@ -236,10 +198,10 @@ func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *Invi
return c.parseInviteAcceptedResponse(resp)
}

func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) {
func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*RemoteUser, error) {
switch r.StatusCode {
case http.StatusOK:
var u User
var u RemoteUser
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
return nil, errors.Wrap(err, "error decoding response body")
}
Expand Down
21 changes: 4 additions & 17 deletions internal/http/services/opencloudmesh/ocmd/invites.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,6 @@ func (h *invitesHandler) init(c *config) error {
return nil
}

type acceptInviteRequest struct {
Token string `json:"token"`
UserID string `json:"userID"`
RecipientProvider string `json:"recipientProvider"`
Name string `json:"name"`
Email string `json:"email"`
}

// AcceptInvite informs avout an accepted invitation so that the users
// can initiate the OCM share creation.
func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
Expand All @@ -68,6 +60,7 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err)
return
}
log.Info().Any("req", req).Msg("OCM /invite-accepted request received")

if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil)
Expand Down Expand Up @@ -138,7 +131,7 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
}
}

if err := json.NewEncoder(w).Encode(&user{
if err := json.NewEncoder(w).Encode(&RemoteUser{
UserID: acceptInviteResponse.UserId.OpaqueId,
Email: acceptInviteResponse.Email,
Name: acceptInviteResponse.DisplayName,
Expand All @@ -152,14 +145,8 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
log.Info().Str("user", fmt.Sprintf("%s@%s", userObj.Id.OpaqueId, userObj.Id.Idp)).Str("token", req.Token).Msg("added to accepted users")
}

type user struct {
UserID string `json:"userID"`
Email string `json:"email"`
Name string `json:"name"`
}

func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) {
var req acceptInviteRequest
func getAcceptInviteRequest(r *http.Request) (*InviteAcceptedRequest, error) {
var req InviteAcceptedRequest
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
Expand Down
Loading