diff --git a/changelog/unreleased/ocm-access.md b/changelog/unreleased/ocm-access.md new file mode 100644 index 0000000000..c99a7e2b68 --- /dev/null +++ b/changelog/unreleased/ocm-access.md @@ -0,0 +1,8 @@ +Enhancement: ocm: support bearer token access + +This PR adds support for accessing remote OCM 1.1 shares via bearer token, +as opposed to having the shared secret in the URL only. +In addition, the OCM client package is now part of the OCMD server package, +and the Discover methods have been all consolidated in one place. + +https://github.com/cs3org/reva/pull/4670 diff --git a/examples/cernbox/cernbox.toml b/examples/cernbox/cernbox.toml index a2091d0de1..3f7b47b30d 100644 --- a/examples/cernbox/cernbox.toml +++ b/examples/cernbox/cernbox.toml @@ -199,7 +199,8 @@ webapp_template = "{{ vars.external_reva_endpoint }}/external/sciencemesh/{{.Tok file = "{{ vars.ocmshares_json_file }}" [grpc.services.ocmproviderauthorizer] -driver = "json" +driver = "open" # pure OCM, all remote shares are accepted +#driver = "json" # to enable sciencemesh [grpc.services.ocmproviderauthorizer.drivers.json] # this is used by the docker-based test deployment, not in production @@ -279,7 +280,7 @@ sender_mail = "sciencemesh@{{ vars.provider_domain }}" smtp_server = "smtp.{{ vars.provider_domain }}" smtp_port = 25 -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] address = ":443" ocm_prefix = "ocm" provider = "Reva for CERNBox" @@ -392,8 +393,6 @@ insecure = true [http.services.prometheus] address = ":443" -[http.services.sysinfo] - #[http.services.ui] #address = ":443" diff --git a/examples/ocm/server-1.toml b/examples/ocm/server-1.toml index a4cb399427..424f5254b6 100644 --- a/examples/ocm/server-1.toml +++ b/examples/ocm/server-1.toml @@ -166,7 +166,7 @@ driver = "ocmreceived" address = "0.0.0.0:8080" expose_recipient_display_name = true -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] ocm_prefix = "ocm" provider = "reva@cern" endpoint = "http://localhost:{{ http.services.ocm.address.port }}" diff --git a/examples/ocm/server-2.toml b/examples/ocm/server-2.toml index 7b9bf24cb3..66a00b2249 100644 --- a/examples/ocm/server-2.toml +++ b/examples/ocm/server-2.toml @@ -166,7 +166,7 @@ driver = "ocmreceived" address = "0.0.0.0:80" expose_recipient_display_name = true -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] ocm_prefix = "ocm" provider = "reva@cesnet" endpoint = "http://localhost:{{ http.services.ocm.address.port }}" diff --git a/examples/sciencemesh/sciencemesh.toml b/examples/sciencemesh/sciencemesh.toml index c39ee95d6a..d9fc44bfa9 100644 --- a/examples/sciencemesh/sciencemesh.toml +++ b/examples/sciencemesh/sciencemesh.toml @@ -253,7 +253,7 @@ sender_mail = "sciencemesh@{{ vars.provider_domain }}" smtp_server = "smtp.{{ vars.provider_domain }}" smtp_port = 25 -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] address = ":443" ocm_prefix = "ocm" provider = "Reva for ownCloud/Nextcloud" @@ -284,7 +284,5 @@ metrics_data_driver_type = "json" metrics_data_location = "/etc/revad/metrics.json" metrics_record_interval = 5000 -[http.services.sysinfo] - [http.middlewares.cors] [http.middlewares.log] diff --git a/examples/standalone/standalone.toml b/examples/standalone/standalone.toml index 5a63df304b..e7e83ad461 100644 --- a/examples/standalone/standalone.toml +++ b/examples/standalone/standalone.toml @@ -17,6 +17,6 @@ [http.services.dataprovider] [http.services.prometheus] [http.services.ocm] -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] [http.services.ocdav] [http.services.ocs] diff --git a/examples/storage-references/gateway.toml b/examples/storage-references/gateway.toml index c57d035e51..fff03471bc 100644 --- a/examples/storage-references/gateway.toml +++ b/examples/storage-references/gateway.toml @@ -57,7 +57,7 @@ app_url = "https://your-collabora-server.org:9980" [http.services.datagateway] [http.services.prometheus] [http.services.ocm] -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] [http.services.ocdav] [http.services.ocs] diff --git a/examples/two-server-setup/gateway-1.toml b/examples/two-server-setup/gateway-1.toml index 1d3414071c..7d5d08731a 100644 --- a/examples/two-server-setup/gateway-1.toml +++ b/examples/two-server-setup/gateway-1.toml @@ -84,7 +84,7 @@ address = "0.0.0.0:19001" [http.services.datagateway] [http.services.prometheus] [http.services.ocm] -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] provider = "Reva-Server-1" endpoint = "http://localhost:19001" enable_webapp = true diff --git a/examples/two-server-setup/gateway-2.toml b/examples/two-server-setup/gateway-2.toml index 8361b3c7e8..bf3f22406d 100644 --- a/examples/two-server-setup/gateway-2.toml +++ b/examples/two-server-setup/gateway-2.toml @@ -84,7 +84,7 @@ address = "0.0.0.0:29001" [http.services.datagateway] [http.services.prometheus] [http.services.ocm] -[http.services.ocmprovider] +[http.services.wellknown.ocmprovider] provider = "Reva-Server-2" endpoint = "http://localhost:29001" enable_webapp = true diff --git a/internal/grpc/services/ocmcore/ocmcore.go b/internal/grpc/services/ocmcore/ocmcore.go index 218e3a3890..81f6f038da 100644 --- a/internal/grpc/services/ocmcore/ocmcore.go +++ b/internal/grpc/services/ocmcore/ocmcore.go @@ -18,6 +18,8 @@ package ocmcore +// This package implements the core OCM API for receiving external shares from remote EFSS systems. + import ( "context" "fmt" @@ -102,7 +104,7 @@ func (s *service) UnprotectedEndpoints() []string { return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"} } -// CreateOCMCoreShare is called when an OCM request comes into this reva instance from. +// CreateOCMCoreShare is called when a remote OCM request comes into this reva instance. func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) { if req.ShareType != ocm.ShareType_SHARE_TYPE_USER { return nil, errtypes.NotSupported("share type not supported") diff --git a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go index ac7956f6ff..a4f0d44039 100644 --- a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go +++ b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go @@ -26,9 +26,9 @@ import ( invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmd" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/ocm/client" "github.com/cs3org/reva/pkg/ocm/invite" "github.com/cs3org/reva/pkg/ocm/invite/repository/registry" "github.com/cs3org/reva/pkg/plugin" @@ -66,7 +66,7 @@ type config struct { type service struct { conf *config repo invite.Repository - ocmClient *client.OCMClient + ocmClient *ocmd.OCMClient } func (c *config) ApplyDefaults() { @@ -110,12 +110,9 @@ func New(ctx context.Context, m map[string]interface{}) (rgrpc.Service, error) { } service := &service{ - conf: &c, - repo: repo, - ocmClient: client.New(&client.Config{ - Timeout: time.Duration(c.OCMClientTimeout) * time.Second, - Insecure: c.OCMClientInsecure, - }), + conf: &c, + repo: repo, + ocmClient: ocmd.NewClient(time.Duration(c.OCMClientTimeout)*time.Second, c.OCMClientInsecure), } return service, nil } @@ -166,7 +163,7 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite return nil, err } - remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{ + remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &ocmd.InviteAcceptedRequest{ Token: req.InviteToken.GetToken(), RecipientProvider: s.conf.ProviderDomain, UserID: user.GetId().GetOpaqueId(), @@ -175,19 +172,19 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite }) if err != nil { switch { - case errors.Is(err, client.ErrTokenInvalid): + case errors.Is(err, ocmd.ErrTokenInvalid): return &invitepb.ForwardInviteResponse{ Status: status.NewInvalid(ctx, "token not valid"), }, nil - case errors.Is(err, client.ErrTokenNotFound): + case errors.Is(err, ocmd.ErrTokenNotFound): return &invitepb.ForwardInviteResponse{ Status: status.NewNotFound(ctx, "token not found"), }, nil - case errors.Is(err, client.ErrUserAlreadyAccepted): + case errors.Is(err, ocmd.ErrUserAlreadyAccepted): return &invitepb.ForwardInviteResponse{ Status: status.NewAlreadyExists(ctx, err, err.Error()), }, nil - case errors.Is(err, client.ErrServiceNotTrusted): + case errors.Is(err, ocmd.ErrServiceNotTrusted): return &invitepb.ForwardInviteResponse{ Status: status.NewPermissionDenied(ctx, err, err.Error()), }, nil diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 1de9c8427c..e26e7b02e5 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -18,6 +18,9 @@ package ocmshareprovider +// This package implements the OCM client API: it allows shares created on this Reva instance +// to be sent to a remote EFSS system via OCM. + import ( "context" "fmt" @@ -38,7 +41,6 @@ import ( "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/ocm/client" "github.com/cs3org/reva/pkg/ocm/share" "github.com/cs3org/reva/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/pkg/plugin" @@ -70,13 +72,13 @@ type config struct { GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"` ProviderDomain string `docs:"The same domain registered in the provider authorizer" mapstructure:"provider_domain" validate:"required"` WebDAVEndpoint string `mapstructure:"webdav_endpoint" validate:"required"` - WebappTemplate string `mapstructure:"webapp_template"` + WebappTemplate string `mapstructure:"webapp_template" validate:"required"` } type service struct { conf *config repo share.Repository - client *client.OCMClient + client *ocmd.OCMClient gateway gateway.GatewayAPIClient webappTmpl *template.Template walker walker.Walker @@ -89,9 +91,6 @@ func (c *config) ApplyDefaults() { if c.ClientTimeout == 0 { c.ClientTimeout = 10 } - if c.WebappTemplate == "" { - c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}" - } c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) } @@ -119,11 +118,6 @@ func New(ctx context.Context, m map[string]interface{}) (rgrpc.Service, error) { return nil, err } - client := client.New(&client.Config{ - Timeout: time.Duration(c.ClientTimeout) * time.Second, - Insecure: c.ClientInsecure, - }) - gateway, err := pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySVC)) if err != nil { return nil, err @@ -135,10 +129,11 @@ func New(ctx context.Context, m map[string]interface{}) (rgrpc.Service, error) { } walker := walker.NewWalker(gateway) + ocmcl := ocmd.NewClient(time.Duration(c.ClientTimeout)*time.Second, c.ClientInsecure) service := &service{ conf: &c, repo: repo, - client: client, + client: ocmcl, gateway: gateway, webappTmpl: tpl, walker: walker, @@ -178,13 +173,14 @@ func getResourceType(info *providerpb.ResourceInfo) string { return "unknown" } -func (s *service) webdavURL(ctx context.Context, share *ocm.Share) string { - // the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token - p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/remote.php/dav/ocm", share.Token) +func (s *service) webdavURL(share *ocm.Share) string { + // the url is expected to be in the form https://ourserver/remote.php/dav/ocm/{ShareId}, see c.WebdavRoot in ocmprovider.go + // TODO(lopresti) take the root from http.services.wellknown.ocmprovider's config + p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/remote.php/dav/ocm", share.Id.OpaqueId) return p } -func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { +func (s *service) getWebdavProtocol(share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { var perms []string if m.WebdavOptions.Permissions.InitiateFileDownload { perms = append(perms, "read") @@ -195,7 +191,7 @@ func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *oc return &ocmd.WebDAV{ Permissions: perms, - URL: s.webdavURL(ctx, share), + URL: s.webdavURL(share), SharedSecret: share.Token, } } @@ -233,7 +229,7 @@ func (s *service) getDataTransferProtocol(ctx context.Context, share *ocm.Share) panic(err) } return &ocmd.Datatx{ - SourceURI: s.webdavURL(ctx, share), + SourceURI: s.webdavURL(share), Size: size, } } @@ -248,7 +244,7 @@ func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Proto for _, m := range share.AccessMethods { switch t := m.Term.(type) { case *ocm.AccessMethod_WebdavOptions: - p = append(p, s.getWebdavProtocol(ctx, share, t)) + p = append(p, s.getWebdavProtocol(share, t)) case *ocm.AccessMethod_WebappOptions: p = append(p, s.getWebappProtocol(share)) case *ocm.AccessMethod_TransferOptions: @@ -323,7 +319,7 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq }, nil } - newShareReq := &client.NewShareRequest{ + newShareReq := &ocmd.NewShareRequest{ ShareWith: formatOCMUser(req.Grantee.GetUserId()), Name: ocmshare.Name, ProviderID: ocmshare.Id.OpaqueId, @@ -348,11 +344,11 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq newShareRes, err := s.client.NewShare(ctx, ocmEndpoint, newShareReq) if err != nil { switch { - case errors.Is(err, client.ErrInvalidParameters): + case errors.Is(err, ocmd.ErrInvalidParameters): return &ocm.CreateOCMShareResponse{ Status: status.NewInvalidArg(ctx, err.Error()), }, nil - case errors.Is(err, client.ErrServiceNotTrusted): + case errors.Is(err, ocmd.ErrServiceNotTrusted): return &ocm.CreateOCMShareResponse{ Status: status.NewInvalidArg(ctx, err.Error()), }, nil diff --git a/internal/http/services/loader/loader.go b/internal/http/services/loader/loader.go index afa0e2e4c8..20982876df 100644 --- a/internal/http/services/loader/loader.go +++ b/internal/http/services/loader/loader.go @@ -29,7 +29,6 @@ import ( _ "github.com/cs3org/reva/internal/http/services/helloworld" _ "github.com/cs3org/reva/internal/http/services/metrics" _ "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmd" - _ "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmprovider" _ "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" _ "github.com/cs3org/reva/internal/http/services/owncloud/ocs" _ "github.com/cs3org/reva/internal/http/services/pingpong" @@ -37,5 +36,6 @@ import ( _ "github.com/cs3org/reva/internal/http/services/pprof" _ "github.com/cs3org/reva/internal/http/services/preferences" _ "github.com/cs3org/reva/internal/http/services/prometheus" + _ "github.com/cs3org/reva/internal/http/services/wellknown" // Add your own service here. ) diff --git a/pkg/ocm/client/client.go b/internal/http/services/opencloudmesh/ocmd/client.go similarity index 76% rename from pkg/ocm/client/client.go rename to internal/http/services/opencloudmesh/ocmd/client.go index faeb28d183..106f51e5ad 100644 --- a/pkg/ocm/client/client.go +++ b/internal/http/services/opencloudmesh/ocmd/client.go @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package client +package ocmd import ( "bytes" @@ -28,7 +28,7 @@ import ( "net/url" "time" - "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmd" + "github.com/cs3org/reva/internal/http/services/wellknown" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/pkg/errors" @@ -59,62 +59,28 @@ type OCMClient struct { client *http.Client } -// Config is the configuration to be used for the OCMClient. -type Config struct { - Timeout time.Duration - Insecure bool -} - -// New returns a new OCMClient. -func New(c *Config) *OCMClient { +// NewClient returns a new OCMClient. +func NewClient(timeout time.Duration, insecure bool) *OCMClient { tr := &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: c.Insecure}, + TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure}, } return &OCMClient{ - client: &http.Client{Transport: tr}, - } -} - -// 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 + client: &http.Client{ + Transport: tr, + Timeout: timeout, + }, } - return &b, nil } -// InviteAccepted informs the sender 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) { - url, err := url.JoinPath(endpoint, "invite-accepted") - if err != nil { - return nil, err - } - - body, err := r.toJSON() +// 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") if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return nil, errors.Wrap(err, "error creating request") } @@ -126,48 +92,36 @@ func (c *OCMClient) InviteAccepted(ctx context.Context, endpoint string, r *Invi } defer resp.Body.Close() - return c.parseInviteAcceptedResponse(resp) -} - -func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) { - switch r.StatusCode { - case http.StatusOK: - var u User - if err := json.NewDecoder(r.Body).Decode(&u); err != nil { - return nil, errors.Wrap(err, "error decoding response body") - } - return &u, nil - case http.StatusBadRequest: - return nil, ErrTokenInvalid - case http.StatusNotFound: - return nil, ErrTokenNotFound - case http.StatusConflict: - return nil, ErrUserAlreadyAccepted - case http.StatusForbidden: - return nil, ErrServiceNotTrusted + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err } - body, err := io.ReadAll(r.Body) + var disco wellknown.OcmDiscoveryData + err = json.Unmarshal(body, &disco) if err != nil { - return nil, errors.Wrap(err, "error decoding response body") + 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 nil, errtypes.InternalError(string(body)) + + 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 ocmd.Protocols `json:"protocol"` + 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) { @@ -197,7 +151,7 @@ func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRe } log := appctx.GetLogger(ctx) - log.Debug().Msgf("Sending OCM /shares POST to %s: %s", url, body) + log.Info().Str("url", url).Msgf("Sending OCM share: %s", body) req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return nil, errors.Wrap(err, "error creating request") @@ -232,34 +186,46 @@ func (c *OCMClient) parseNewShareResponse(r *http.Response) (*NewShareResponse, return nil, errtypes.InternalError(string(body)) } -// Capabilities contains a set of properties exposed by -// a remote cloud storage. -type Capabilities struct { - Enabled bool `json:"enabled"` - APIVersion string `json:"apiVersion"` - EndPoint string `json:"endPoint"` - Provider string `json:"provider"` - ResourceTypes []struct { - Name string `json:"name"` - ShareTypes []string `json:"shareTypes"` - Protocols struct { - Webdav *string `json:"webdav"` - Webapp *string `json:"webapp"` - Datatx *string `json:"datatx"` - } `json:"protocols"` - } `json:"resourceTypes"` - Capabilities []string `json:"capabilities"` +// 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"` } -// Discovery 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) Discovery(ctx context.Context, endpoint string) (*Capabilities, error) { - url, err := url.JoinPath(endpoint, "shares") +// 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 +// 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) { + url, err := url.JoinPath(endpoint, "invite-accepted") if err != nil { return nil, err } - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + body, err := r.toJSON() + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body) if err != nil { return nil, errors.Wrap(err, "error creating request") } @@ -271,10 +237,30 @@ func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabiliti } defer resp.Body.Close() - var cap Capabilities - if err := json.NewDecoder(resp.Body).Decode(&c); err != nil { - return nil, err + return c.parseInviteAcceptedResponse(resp) +} + +func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) { + switch r.StatusCode { + case http.StatusOK: + var u User + if err := json.NewDecoder(r.Body).Decode(&u); err != nil { + return nil, errors.Wrap(err, "error decoding response body") + } + return &u, nil + case http.StatusBadRequest: + return nil, ErrTokenInvalid + case http.StatusNotFound: + return nil, ErrTokenNotFound + case http.StatusConflict: + return nil, ErrUserAlreadyAccepted + case http.StatusForbidden: + return nil, ErrServiceNotTrusted } - return &cap, nil + body, err := io.ReadAll(r.Body) + if err != nil { + return nil, errors.Wrap(err, "error decoding response body") + } + return nil, errtypes.InternalError(string(body)) } diff --git a/internal/http/services/opencloudmesh/ocmd/shares.go b/internal/http/services/opencloudmesh/ocmd/shares.go index a48f040f7e..954e72ccae 100644 --- a/internal/http/services/opencloudmesh/ocmd/shares.go +++ b/internal/http/services/opencloudmesh/ocmd/shares.go @@ -21,7 +21,6 @@ package ocmd import ( "encoding/json" "fmt" - "io" "mime" "net/http" "path/filepath" @@ -37,13 +36,11 @@ import ( ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" - ocmproviderhttp "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmprovider" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/cs3org/reva/internal/http/services/reqres" "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" - "github.com/cs3org/reva/pkg/httpclient" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/utils" "github.com/go-playground/validator/v10" @@ -282,43 +279,21 @@ func discoverOcmWebdavRoot(r *http.Request) (string, error) { log := appctx.GetLogger(ctx) log.Debug().Str("sender", r.Host).Msg("received OCM 1.0 share, attempting to discover sender endpoint") - httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, r.Host+"/ocm-provider", nil) + ocmClient := NewClient(time.Duration(10)*time.Second, true) + ocmCaps, err := ocmClient.Discover(ctx, r.Host) if err != nil { + log.Warn().Str("sender", r.Host).Err(err).Msg("failed to discover OCM sender") return "", err } - httpClient := httpclient.New( - httpclient.Timeout(time.Duration(10 * int64(time.Second))), - ) - httpRes, err := httpClient.Do(httpReq) - if err != nil { - return "", errors.Wrap(err, "failed to contact OCM sender server") - } - defer httpRes.Body.Close() - - if httpRes.StatusCode != http.StatusOK { - return "", errtypes.InternalError("Invalid HTTP response on OCM discovery") - } - body, err := io.ReadAll(httpRes.Body) - if err != nil { - return "", err - } - - var result ocmproviderhttp.DiscoveryData - err = json.Unmarshal(body, &result) - if err != nil { - log.Warn().Str("sender", r.Host).Str("response", string(body)).Msg("malformed response") - return "", errtypes.InternalError("Invalid payload on OCM discovery") - } - - for _, t := range result.ResourceTypes { + for _, t := range ocmCaps.ResourceTypes { webdavRoot, ok := t.Protocols["webdav"] if ok { // assume the first resourceType that exposes a webdav root is OK to use: as a matter of fact, // no implementation exists yet that exposes multiple resource types with different roots. - return filepath.Join(result.Endpoint, webdavRoot), nil + return filepath.Join(ocmCaps.Endpoint, webdavRoot), nil } } - log.Warn().Str("sender", r.Host).Str("response", string(body)).Msg("missing webdav root") + log.Warn().Str("sender", r.Host).Interface("response", ocmCaps).Msg("missing webdav root") return "", errtypes.NotFound("WebDAV root not found on OCM discovery") } diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index ed0ea135bd..2532b63b67 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -188,24 +188,36 @@ func (h *DavHandler) Handler(s *svc) http.Handler { return } - // OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the - // shared token. - var token string - username, _, ok := r.BasicAuth() - if ok { - // OCM 1.0 - token = username - r.URL.Path = filepath.Join("/", token, r.URL.Path) - ctx = context.WithValue(ctx, ctxOCM10, true) + var token, ocmshare string + // OCM v1.1 (OCIS et al.). + bearer := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ") + if bearer != "" { + // Bearer token is the shared secret, path is /{shareId}/path/to/resource. + // Here we're keeping the simpler public-share model, where the internal routing is done via the token, + // therefore we strip the shareId and reinject the token. + // TODO(lopresti) We should instead perform a lookup via shareId and leave the token just for auth. + var relPath string + token = bearer + ocmshare, relPath = router.ShiftPath(r.URL.Path) + r.URL.Path = filepath.Join("/", token, relPath) } else { - token, _ = router.ShiftPath(r.URL.Path) - ctx = context.WithValue(ctx, ctxOCM10, false) + username, _, ok := r.BasicAuth() + if ok { + // OCM v1.0 (OC10 and Nextcloud) uses basic auth for carrying the shared secret, + // and does not pass the shareId. + token = username + r.URL.Path = filepath.Join("/", token, r.URL.Path) + } else { + // compatibility for ScienceMesh: no auth, shared secret is the first element + // of the path, the shareId is not given. Leave the URL as is. + token = strings.Split(r.URL.Path, "/")[1] + } } - authRes, err := handleOCMAuth(ctx, c, token) + authRes, err := handleOCMAuth(ctx, c, ocmshare, token) switch { case err != nil: - log.Error().Err(err).Msg("error during ocm authentication") + log.Error().Err(err).Msg("error during OCM authentication") w.WriteHeader(http.StatusInternalServerError) return case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: @@ -228,6 +240,7 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx = appctx.ContextSetToken(ctx, authRes.Token) ctx = appctx.ContextSetUser(ctx, authRes.User) ctx = metadata.AppendToOutgoingContext(ctx, appctx.TokenHeader, authRes.Token) + ctx = context.WithValue(ctx, ctxOCM, true) log.Debug().Str("token", token).Interface("user", authRes.User).Msg("OCM user authenticated") @@ -347,9 +360,10 @@ func handleSignatureAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, return c.Authenticate(ctx, &authenticateRequest) } -func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token string) (*gatewayv1beta1.AuthenticateResponse, error) { +func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, ocmshare, token string) (*gatewayv1beta1.AuthenticateResponse, error) { return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{ - Type: "ocmshares", - ClientId: token, + Type: "ocmshares", + ClientId: ocmshare, + ClientSecret: token, }) } diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 0a6b49bbce..4e996c1ee4 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -51,7 +51,7 @@ type ctxKey int const ( ctxKeyBaseURI ctxKey = iota - ctxOCM10 + ctxOCM ) var ( @@ -198,7 +198,7 @@ func (s *svc) Close() error { } func (s *svc) Unprotected() []string { - return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/s/", "/remote.php/dav/ocm/"} + return []string{"/status.php", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/s/", "/remote.php/dav/ocm/", "/ocm-provider"} } func (s *svc) Handler() http.Handler { @@ -255,6 +255,10 @@ func (s *svc) Handler() http.Handler { http.Redirect(w, r, rURL, http.StatusMovedPermanently) return } + case "ocm-provider": + // this is to support the current/legacy discovery endpoint for OCM + http.Redirect(w, r, "/.well-known/ocm", http.StatusMovedPermanently) + return } switch head { // the old `/webdav` endpoint uses remote.php/webdav/$path diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 5f532a3fd8..d762f63edf 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -504,16 +504,6 @@ func (s *svc) newPropRaw(key, val string) *propertyXML { } } -func supportLegacyOCMAccess(ctx context.Context, md *provider.ResourceInfo) { - ocm10, _ := ctx.Value(ctxOCM10).(bool) - if ocm10 { - // the path is something like //... - // we need to strip the token part as this - // is passed as username in the basic auth - _, md.Path = router.ShiftPath(md.Path) - } -} - func appendSlash(path string) string { if path == "" { return "/" @@ -540,11 +530,15 @@ func (s *svc) isOpenable(path string) bool { // prefixing it with the baseURI. func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provider.ResourceInfo, ns string, usershares, linkshares map[string]struct{}) (*responseXML, error) { sublog := appctx.GetLogger(ctx).With().Str("ns", ns).Logger() + md.Path = strings.TrimPrefix(md.Path, ns) + ocm, _ := ctx.Value(ctxOCM).(bool) + if ocm { + // // was injected in front of the OCM path for the routing to work, we now remove it (see internal/http/services/owncloud/ocdav/dav.go) + _, md.Path = router.ShiftPath(md.Path) + } baseURI := ctx.Value(ctxKeyBaseURI).(string) - - supportLegacyOCMAccess(ctx, md) ref := path.Join(baseURI, md.Path) if md.Type == provider.ResourceType_RESOURCE_TYPE_CONTAINER { ref += "/" diff --git a/internal/http/services/opencloudmesh/ocmprovider/ocmprovider.go b/internal/http/services/wellknown/ocm.go similarity index 71% rename from internal/http/services/opencloudmesh/ocmprovider/ocmprovider.go rename to internal/http/services/wellknown/ocm.go index 539c7aefca..f8f4d53612 100644 --- a/internal/http/services/opencloudmesh/ocmprovider/ocmprovider.go +++ b/internal/http/services/wellknown/ocm.go @@ -16,27 +16,20 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package ocmprovider +package wellknown import ( - "context" "encoding/json" "net/http" "net/url" "path/filepath" "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/rhttp/global" - "github.com/cs3org/reva/pkg/utils/cfg" ) const OCMAPIVersion = "1.1.0" -func init() { - global.Register("ocmprovider", New) -} - -type config struct { +type OcmProviderConfig struct { OCMPrefix string `docs:"ocm;The prefix URL where the OCM API is served." mapstructure:"ocm_prefix"` Endpoint string `docs:"This host's full URL. If it's not configured, it is assumed OCM is not available." mapstructure:"endpoint"` Provider string `docs:"reva;A friendly name that defines this service." mapstructure:"provider"` @@ -46,7 +39,7 @@ type config struct { EnableDatatx bool `docs:"false;Whether data transfers are enabled in OCM shares." mapstructure:"enable_datatx"` } -type DiscoveryData struct { +type OcmDiscoveryData struct { Enabled bool `json:"enabled" xml:"enabled"` APIVersion string `json:"apiVersion" xml:"apiVersion"` Endpoint string `json:"endPoint" xml:"endPoint"` @@ -61,11 +54,11 @@ type resourceTypes struct { Protocols map[string]string `json:"protocols"` } -type svc struct { - data *DiscoveryData +type wkocmHandler struct { + data *OcmDiscoveryData } -func (c *config) ApplyDefaults() { +func (c *OcmProviderConfig) ApplyDefaults() { if c.OCMPrefix == "" { c.OCMPrefix = "ocm" } @@ -86,10 +79,11 @@ func (c *config) ApplyDefaults() { } } -func (c *config) prepare() *DiscoveryData { - // generates the (static) data structure to be exposed by /ocm-provider: +func (h *wkocmHandler) init(c *OcmProviderConfig) { + // generates the (static) data structure to be exposed by /.well-known/ocm: // first prepare an empty and disabled payload - d := &DiscoveryData{} + c.ApplyDefaults() + d := &OcmDiscoveryData{} d.Enabled = false d.Endpoint = "" d.APIVersion = OCMAPIVersion @@ -102,12 +96,14 @@ func (c *config) prepare() *DiscoveryData { d.Capabilities = []string{} if c.Endpoint == "" { - return d + h.data = d + return } endpointURL, err := url.Parse(c.Endpoint) if err != nil { - return d + h.data = d + return } // now prepare the enabled one @@ -129,49 +125,24 @@ func (c *config) prepare() *DiscoveryData { }} // for now we hardcode the capabilities, as this is currently only advisory d.Capabilities = []string{"/invite-accepted"} - return d + h.data = d } -// New returns a new ocmprovider object, that implements -// the OCM discovery endpoint specified in +// This handler implements the OCM discovery endpoint specified in // https://cs3org.github.io/OCM-API/docs.html?repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get -func New(ctx context.Context, m map[string]interface{}) (global.Service, error) { - var c config - if err := cfg.Decode(m, &c); err != nil { - return nil, err +func (h *wkocmHandler) Ocm(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if r.UserAgent() == "Nextcloud Server Crawler" { + // Nextcloud decided to only support OCM 1.0 and 1.1, not any 1.x as per SemVer. See + // https://github.com/nextcloud/server/pull/39574#issuecomment-1679191188 + h.data.APIVersion = "1.1" + } else { + h.data.APIVersion = OCMAPIVersion + } + indented, _ := json.MarshalIndent(h.data, "", " ") + if _, err := w.Write(indented); err != nil { + log.Err(err).Msg("Error writing to ResponseWriter") } - return &svc{data: c.prepare()}, nil -} - -// Close performs cleanup. -func (s *svc) Close() error { - return nil -} - -func (s *svc) Prefix() string { - // this is hardcoded as per OCM specifications - return "/ocm-provider" -} - -func (s *svc) Unprotected() []string { - return []string{"/"} -} - -func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if r.UserAgent() == "Nextcloud Server Crawler" { - // Nextcloud decided to only support OCM 1.0 and 1.1, not any 1.x as per SemVer. See - // https://github.com/nextcloud/server/pull/39574#issuecomment-1679191188 - s.data.APIVersion = "1.1" - } else { - s.data.APIVersion = OCMAPIVersion - } - indented, _ := json.MarshalIndent(s.data, "", " ") - if _, err := w.Write(indented); err != nil { - log.Err(err).Msg("Error writing to ResponseWriter") - } - }) } diff --git a/internal/http/services/wellknown/wellknown.go b/internal/http/services/wellknown/wellknown.go new file mode 100644 index 0000000000..5ca2c2fb0d --- /dev/null +++ b/internal/http/services/wellknown/wellknown.go @@ -0,0 +1,91 @@ +// Copyright 2018-2024 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package wellknown + +import ( + "context" + "net/http" + + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/rhttp/global" + "github.com/cs3org/reva/pkg/utils/cfg" + "github.com/go-chi/chi/v5" +) + +func init() { + global.Register("wellknown", New) +} + +type svc struct { + router chi.Router + Conf *config +} + +type config struct { + OCMProvider OcmProviderConfig `mapstructure:"ocmprovider"` +} + +// New returns a new wellknown object. +func New(ctx context.Context, m map[string]interface{}) (global.Service, error) { + var c config + if err := cfg.Decode(m, &c); err != nil { + return nil, err + } + + r := chi.NewRouter() + s := &svc{ + router: r, + Conf: &c, + } + if err := s.routerInit(); err != nil { + return nil, err + } + + return s, nil +} + +func (s *svc) routerInit() error { + wkocmHandler := new(wkocmHandler) + wkocmHandler.init(&s.Conf.OCMProvider) + s.router.Get("/ocm", wkocmHandler.Ocm) + return nil +} + +func (s *svc) Close() error { + return nil +} + +func (s *svc) Prefix() string { + return "/.well-known" +} + +func (s *svc) Unprotected() []string { + return []string{"/", "/ocm"} +} + +func (s *svc) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + log.Debug().Str("path", r.URL.Path).Msg(".well-known routing") + + // unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments + r.URL.RawPath = "" + s.router.ServeHTTP(w, r) + }) +} diff --git a/pkg/auth/manager/ocmshares/ocmshares.go b/pkg/auth/manager/ocmshares/ocmshares.go index c53b00a085..b347cbe817 100644 --- a/pkg/auth/manager/ocmshares/ocmshares.go +++ b/pkg/auth/manager/ocmshares/ocmshares.go @@ -82,8 +82,8 @@ func (m *manager) Configure(ml map[string]interface{}) error { return nil } -func (m *manager) Authenticate(ctx context.Context, token, _ string) (*userpb.User, map[string]*authpb.Scope, error) { - log := appctx.GetLogger(ctx).With().Str("token", token).Logger() +func (m *manager) Authenticate(ctx context.Context, ocmshare, token string) (*userpb.User, map[string]*authpb.Scope, error) { + log := appctx.GetLogger(ctx).With().Str("token", token).Str("ocmshare", ocmshare).Logger() shareRes, err := m.gw.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{ Token: token, }) @@ -103,6 +103,12 @@ func (m *manager) Authenticate(ctx context.Context, token, _ string) (*userpb.Us return nil, nil, errtypes.InternalError(shareRes.Status.Message) } + // validate OCM share id if given (OCM v1.1) + if ocmshare != "" && shareRes.GetShare().GetId().GetOpaqueId() != ocmshare { + log.Error().Str("requested_share", ocmshare).Str("share_from_provider", shareRes.GetShare().GetId().GetOpaqueId()).Msg("mismatching ocm share id for existing secret") + return nil, nil, errtypes.InvalidCredentials("invalid shared secret") + } + // the user authenticated using the ocmshares authentication method // is the recipient of the share u := shareRes.Share.Grantee.GetUserId() diff --git a/pkg/ocm/provider/authorizer/open/open.go b/pkg/ocm/provider/authorizer/open/open.go index 88516d6d3d..d0af08efb6 100644 --- a/pkg/ocm/provider/authorizer/open/open.go +++ b/pkg/ocm/provider/authorizer/open/open.go @@ -20,15 +20,17 @@ package open import ( "context" - "encoding/json" - "os" + "net/url" + "path/filepath" "strings" + "time" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - "github.com/cs3org/reva/pkg/errtypes" + client "github.com/cs3org/reva/internal/http/services/opencloudmesh/ocmd" "github.com/cs3org/reva/pkg/ocm/provider" "github.com/cs3org/reva/pkg/ocm/provider/authorizer/registry" "github.com/cs3org/reva/pkg/utils/cfg" + "github.com/pkg/errors" ) func init() { @@ -42,19 +44,7 @@ func New(ctx context.Context, m map[string]interface{}) (provider.Authorizer, er return nil, err } - f, err := os.ReadFile(c.Providers) - if err != nil { - return nil, err - } - providers := []*ocmprovider.ProviderInfo{} - err = json.Unmarshal(f, &providers) - if err != nil { - return nil, err - } - a := &authorizer{} - a.providers = a.getOCMProviders(providers) - return a, nil } @@ -64,9 +54,6 @@ type config struct { } func (c *config) ApplyDefaults() { - if c.Providers == "" { - c.Providers = "/etc/revad/ocm-providers.json" - } } type authorizer struct { @@ -79,7 +66,51 @@ func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmpr return p, nil } } - return nil, errtypes.NotFound(domain) + + // not yet known: try to discover the remote OCM endpoint + ocmClient := client.NewClient(time.Duration(10)*time.Second, true) + ocmCaps, err := ocmClient.Discover(ctx, domain) + if err != nil { + return nil, errors.Wrap(err, "error probing OCM services at remote server") + } + var path string + for _, t := range ocmCaps.ResourceTypes { + webdavRoot, ok := t.Protocols["webdav"] + if ok { + // assume the first resourceType that exposes a webdav root is OK to use: as a matter of fact, + // no implementation exists yet that exposes multiple resource types with different roots. + path = filepath.Join(ocmCaps.Endpoint, webdavRoot) + } + } + host, _ := url.Parse(ocmCaps.Endpoint) + + // return a provider info record for this domain, including the OCM service + return &ocmprovider.ProviderInfo{ + Name: "ocm_" + domain, + FullName: ocmCaps.Provider, + Description: "OCM service at " + domain, + Organization: domain, + Domain: domain, + Homepage: "", + Email: "", + Properties: map[string]string{}, + Services: []*ocmprovider.Service{ + { + Endpoint: &ocmprovider.ServiceEndpoint{ + Type: &ocmprovider.ServiceType{Name: "OCM"}, + Path: ocmCaps.Endpoint, + }, + Host: host.Hostname(), + }, + { + Endpoint: &ocmprovider.ServiceEndpoint{ + Type: &ocmprovider.ServiceType{Name: "Webdav"}, + Path: path, + }, + Host: host.Hostname(), + }, + }, + }, nil } func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovider.ProviderInfo) error { @@ -89,22 +120,3 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, provider *ocmprovide func (a *authorizer) ListAllProviders(ctx context.Context) ([]*ocmprovider.ProviderInfo, error) { return a.providers, nil } - -func (a *authorizer) getOCMProviders(providers []*ocmprovider.ProviderInfo) (po []*ocmprovider.ProviderInfo) { - for _, p := range providers { - _, err := a.getOCMHost(p) - if err == nil { - po = append(po, p) - } - } - return -} - -func (a *authorizer) getOCMHost(provider *ocmprovider.ProviderInfo) (string, error) { - for _, s := range provider.Services { - if s.Endpoint.Type.Name == "OCM" { - return s.Host, nil - } - } - return "", errtypes.NotFound("OCM Host") -} diff --git a/pkg/ocm/share/sender/sender.go b/pkg/ocm/share/sender/sender.go deleted file mode 100644 index 05d6890bde..0000000000 --- a/pkg/ocm/share/sender/sender.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2018-2024 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package sender - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "net/url" - "path" - "strings" - "time" - - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - "github.com/cs3org/reva/pkg/appctx" - "github.com/cs3org/reva/pkg/httpclient" - "github.com/pkg/errors" -) - -const createOCMCoreShareEndpoint = "shares" - -func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) { - for _, s := range originProvider.Services { - if s.Endpoint.Type.Name == "OCM" { - return s.Endpoint.Path, nil - } - } - return "", errors.New("json: ocm endpoint not specified for mesh provider") -} - -// Send executes the POST to the OCM shares endpoint to create the share at the -// remote site. -func Send(ctx context.Context, requestBodyMap map[string]interface{}, pi *ocmprovider.ProviderInfo) error { - requestBody, err := json.Marshal(requestBodyMap) - if err != nil { - err = errors.Wrap(err, "error marshalling request body") - return err - } - ocmEndpoint, err := getOCMEndpoint(pi) - if err != nil { - return err - } - u, err := url.Parse(ocmEndpoint) - if err != nil { - return err - } - u.Path = path.Join(u.Path, createOCMCoreShareEndpoint) - recipientURL := u.String() - - log := appctx.GetLogger(ctx) - log.Info().Msgf("in OCM Send! %s %s", recipientURL, requestBody) - - req, err := http.NewRequest(http.MethodPost, recipientURL, strings.NewReader(string(requestBody))) - if err != nil { - return errors.Wrap(err, "sender: error framing post request") - } - req.Header.Set("Content-Type", "application/json") - client := httpclient.New( - httpclient.Timeout(5 * time.Second), - ) - - resp, err := client.Do(req) - if err != nil { - err = errors.Wrap(err, "sender: error sending post request") - return err - } - - defer resp.Body.Close() - if (resp.StatusCode != http.StatusCreated) && (resp.StatusCode != http.StatusOK) { - respBody, e := io.ReadAll(resp.Body) - if e != nil { - e = errors.Wrap(e, "sender: error reading request body") - return e - } - err = errors.Wrap(fmt.Errorf("%s: %s", resp.Status, string(respBody)), "sender: error from "+ocmEndpoint) - return err - } - return nil -} diff --git a/pkg/ocm/storage/outcoming/ocm.go b/pkg/ocm/storage/outcoming/ocm.go index ed843a8b20..ac80120f7c 100644 --- a/pkg/ocm/storage/outcoming/ocm.go +++ b/pkg/ocm/storage/outcoming/ocm.go @@ -68,6 +68,7 @@ func (c *config) ApplyDefaults() { } // New creates an OCM storage driver. +// This driver exposes local resources to remote OCM users. func New(ctx context.Context, m map[string]interface{}) (storage.FS, error) { var c config if err := cfg.Decode(m, &c); err != nil { @@ -148,6 +149,9 @@ func (d *driver) shareAndRelativePathFromRef(ctx context.Context, ref *provider. } path = makeRelative(path) + log := appctx.GetLogger(ctx) + log.Info().Interface("ref", ref).Str("path", path).Str("token", token).Msg("Accessing OCM share") + share, err := d.resolveToken(ctx, token) if err != nil { return nil, "", err diff --git a/pkg/ocm/storage/received/ocm.go b/pkg/ocm/storage/received/ocm.go index bcb684b9a8..89e73ef29b 100644 --- a/pkg/ocm/storage/received/ocm.go +++ b/pkg/ocm/storage/received/ocm.go @@ -33,6 +33,7 @@ import ( provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typepb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/internal/http/services/owncloud/ocdav" + "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/mime" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" @@ -62,6 +63,7 @@ func (c *config) ApplyDefaults() { } // New creates an OCM storage driver. +// This driver exposes remote OCM resources to local users. func New(ctx context.Context, m map[string]interface{}) (storage.FS, error) { var c config if err := cfg.Decode(m, &c); err != nil { @@ -153,11 +155,12 @@ func (d *driver) webdavClient(ctx context.Context, ref *provider.Reference) (*go return nil, nil, "", err } - // FIXME: it's still not clear from the OCM APIs how to use the shared secret - // will use as a token in the bearer authentication as this is the reva implementation + // use the secret as bearer authentication according to OCM v1.1+ c := gowebdav.NewClient(endpoint, "", "") c.SetHeader("Authorization", "Bearer "+secret) + log := appctx.GetLogger(ctx) + log.Info().Str("endpoint", endpoint).Interface("share", share).Str("rel", rel).Str("secret", secret).Msg("Accessing OCM share") return c, share, rel, nil } diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml index c2c077b74c..9e40fe48f6 100644 --- a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cernbox-grpc.toml @@ -58,6 +58,7 @@ providers = "{{file_providers}}" [grpc.services.ocmshareprovider] driver = "json" webdav_endpoint = "http://{{cernboxwebdav_address}}" +webapp_template = "http://{{cernboxwebdav_address}}/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}" provider_domain = "cernbox.cern.ch" [grpc.services.ocmshareprovider.drivers.json] diff --git a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml index 3a225319ab..399b310561 100644 --- a/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml +++ b/tests/integration/grpc/fixtures/ocm-share/ocm-server-cesnet-grpc.toml @@ -38,6 +38,7 @@ providers = "{{file_providers}}" [grpc.services.ocmshareprovider] driver = "json" webdav_endpoint = "http://{{cesnethttp_address}}" +webapp_template = "http://{{cesnethttp_address}}/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}" provider_domain = "cesnet.cz" [grpc.services.ocmshareprovider.drivers.json] diff --git a/tests/integration/grpc/ocm_share_test.go b/tests/integration/grpc/ocm_share_test.go index fabcb593ee..a560528179 100644 --- a/tests/integration/grpc/ocm_share_test.go +++ b/tests/integration/grpc/ocm_share_test.go @@ -219,6 +219,7 @@ var _ = Describe("ocm share", func() { Expect(ok).To(BeTrue()) webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") + webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) d, err := webdavClient.Read(".") Expect(err).ToNot(HaveOccurred()) Expect(d).To(Equal([]byte("test"))) @@ -299,6 +300,7 @@ var _ = Describe("ocm share", func() { Expect(ok).To(BeTrue()) webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") + webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) data := []byte("new-content") webdavClient.SetHeader(ocdav.HeaderUploadLength, strconv.Itoa(len(data))) err = webdavClient.Write(".", data, 0) @@ -394,7 +396,7 @@ var _ = Describe("ocm share", func() { Expect(ok).To(BeTrue()) webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") - + webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) ok, err = helpers.SameContentWebDAV(webdavClient, fileToShare.Path, structure) Expect(err).ToNot(HaveOccurred()) Expect(ok).To(BeTrue()) @@ -499,6 +501,7 @@ var _ = Describe("ocm share", func() { webdavClient := gowebdav.NewClient(webdav.WebdavOptions.Uri, "", "") data := []byte("new-content") webdavClient.SetHeader(ocdav.HeaderUploadLength, strconv.Itoa(len(data))) + webdavClient.SetHeader("Authorization", "Bearer "+webdav.WebdavOptions.SharedSecret) err = webdavClient.Write("new-file", data, 0) Expect(err).ToNot(HaveOccurred())