diff --git a/changelog/unreleased/ocm-10.md b/changelog/unreleased/ocm-10.md new file mode 100644 index 0000000000..a3e108dbe5 --- /dev/null +++ b/changelog/unreleased/ocm-10.md @@ -0,0 +1,7 @@ +Enhancement: Support incoming OCM 1.0 shares + +OCM 1.0 payloads are now supported as incoming shares, and +converted to the OCM 1.1 format for persistency and further processing. +Outgoing shares are still only OCM 1.1. + +https://github.com/cs3org/reva/pull/4195 diff --git a/internal/http/services/ocmd/ocm.go b/internal/http/services/ocmd/ocm.go index 13c901539a..050f9d83b4 100644 --- a/internal/http/services/ocmd/ocm.go +++ b/internal/http/services/ocmd/ocm.go @@ -35,7 +35,7 @@ func init() { type config struct { Prefix string `mapstructure:"prefix"` - GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` + GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"` } diff --git a/internal/http/services/ocmd/protocols.go b/internal/http/services/ocmd/protocols.go index 33cf2799f6..aff9f4c3cf 100644 --- a/internal/http/services/ocmd/protocols.go +++ b/internal/http/services/ocmd/protocols.go @@ -113,14 +113,30 @@ func (p *Protocols) UnmarshalJSON(data []byte) error { for name, d := range prot { var res Protocol - // we do not support the OCM v1.0 properties for now, therefore just skip or bail out if name == "name" { continue } if name == "options" { var opt map[string]any - if err := json.Unmarshal(d, &opt); err != nil || len(opt) > 0 { - return fmt.Errorf("protocol options not supported: %s", string(d)) + if err := json.Unmarshal(d, &opt); err != nil { + return fmt.Errorf("malformed protocol options %s", d) + } + if len(opt) > 0 { + // This is an OCM 1.0 payload: parse the secret and assume max + // permissions, as in the OCM 1.0 model the remote server would check + // (and would not tell to the sharee!) which permissions are enabled + // on the share. Also, in this case the URL has to be resolved via + // discovery, see shares.go. + ss, ok := opt["sharedSecret"].(string) + if !ok { + return fmt.Errorf("missing sharedSecret from options %s", d) + } + res = &WebDAV{ + SharedSecret: ss, + Permissions: []string{"read", "write", "share"}, + URL: "", + } + *p = append(*p, res) } continue } @@ -145,15 +161,17 @@ func (p Protocols) MarshalJSON() ([]byte, error) { } d := make(map[string]any) for _, prot := range p { - d[getProtocolName(prot)] = prot + d[GetProtocolName(prot)] = prot } - // fill in the OCM v1.0 properties + // fill in the OCM v1.0 properties: for now we only create OCM 1.1 payloads, + // irrespective from the capabilities of the remote server. d["name"] = "multi" d["options"] = map[string]any{} return json.Marshal(d) } -func getProtocolName(p Protocol) string { +// GetProtocolName returns the name of the protocol by reflection. +func GetProtocolName(p Protocol) string { n := reflect.TypeOf(p).String() s := strings.Split(n, ".") return strings.ToLower(s[len(s)-1]) diff --git a/internal/http/services/ocmd/protocols_test.go b/internal/http/services/ocmd/protocols_test.go index 35d857f062..acfe70a3d5 100644 --- a/internal/http/services/ocmd/protocols_test.go +++ b/internal/http/services/ocmd/protocols_test.go @@ -40,13 +40,23 @@ func TestUnmarshalProtocol(t *testing.T) { raw: `{"name":"foo","options":{ }}`, expected: []Protocol{}, }, + { + raw: `{"unsupported":{}}`, + err: "protocol unsupported not recognised", + }, { raw: `{"name":"foo","options":{"unsupported":"value"}}`, - err: `protocol options not supported: {"unsupported":"value"}`, + err: `missing sharedSecret from options {"unsupported":"value"}`, }, { - raw: `{"unsupported":{}}`, - err: "protocol unsupported not recognised", + raw: `{"name":"ocm10format","options":{"sharedSecret":"secret"}}`, + expected: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read", "write", "share"}, + URL: "", + }, + }, }, { raw: `{"name":"multi","options":{},"webdav":{"sharedSecret":"secret","permissions":["read","write"],"url":"http://example.org"}}`, diff --git a/internal/http/services/ocmd/shares.go b/internal/http/services/ocmd/shares.go index 6a491ba974..8905748ef5 100644 --- a/internal/http/services/ocmd/shares.go +++ b/internal/http/services/ocmd/shares.go @@ -20,25 +20,31 @@ package ocmd import ( "encoding/json" - "errors" "fmt" + "io" "mime" "net/http" + "path/filepath" "strings" + "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/pkg/errors" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" 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/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/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/rhttp" "github.com/cs3org/reva/pkg/utils" "github.com/go-playground/validator/v10" ) @@ -149,6 +155,12 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { return } + protocols, err := getAndResolveProtocols(req.Protocols, r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) + return + } + createShareReq := &ocmcore.CreateOCMCoreShareRequest{ Description: req.Description, Name: req.Name, @@ -158,7 +170,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { ShareWith: userRes.User.Id, ResourceType: getResourceTypeFromOCMRequest(req.ResourceType), ShareType: getOCMShareType(req.ShareType), - Protocols: getProtocols(req.Protocols), + Protocols: protocols, } if req.Expiration != 0 { @@ -246,10 +258,67 @@ func getOCMShareType(t string) ocm.ShareType { return ocm.ShareType_SHARE_TYPE_GROUP } -func getProtocols(p Protocols) []*ocm.Protocol { - prot := make([]*ocm.Protocol, 0, len(p)) +func getAndResolveProtocols(p Protocols, r *http.Request) ([]*ocm.Protocol, error) { + protos := make([]*ocm.Protocol, 0, len(p)) for _, data := range p { - prot = append(prot, data.ToOCMProtocol()) + ocmProto := data.ToOCMProtocol() + if GetProtocolName(data) == "webdav" && ocmProto.GetWebdavOptions().Uri == "" { + // This is an OCM 1.0 payload with only webdav: we need to resolve the remote URL + remoteRoot, err := discoverOcmWebdavRoot(r) + if err != nil { + return nil, err + } + ocmProto.GetWebdavOptions().Uri = filepath.Join(remoteRoot, ocmProto.GetWebdavOptions().SharedSecret) + } + protos = append(protos, ocmProto) + } + return protos, nil +} + +func discoverOcmWebdavRoot(r *http.Request) (string, error) { + // implements the OCM discovery logic to fetch the WebDAV root at the remote host that sent the share, see + // https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1ocm-provider/get + ctx := r.Context() + log := appctx.GetLogger(ctx) + log.Debug().Str("sender", r.Host).Msg("received OCM 1.0 share, attempting to discover sender endpoint") + + httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, r.Host+"/ocm-provider", nil) + if err != nil { + return "", err } - return prot + httpClient := rhttp.GetHTTPClient( + rhttp.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 { + 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 + } + } + + log.Warn().Str("sender", r.Host).Str("response", string(body)).Msg("missing webdav root") + return "", errtypes.NotFound("WebDAV root not found on OCM discovery") } diff --git a/internal/http/services/ocmprovider/ocmprovider.go b/internal/http/services/ocmprovider/ocmprovider.go index 5a2af44ec2..99993b5924 100644 --- a/internal/http/services/ocmprovider/ocmprovider.go +++ b/internal/http/services/ocmprovider/ocmprovider.go @@ -45,7 +45,7 @@ type config struct { EnableDatatx bool `mapstructure:"enable_datatx" docs:"false;Whether data transfers are enabled in OCM shares."` } -type discoveryData struct { +type DiscoveryData struct { Enabled bool `json:"enabled" xml:"enabled"` APIVersion string `json:"apiVersion" xml:"apiVersion"` Endpoint string `json:"endPoint" xml:"endPoint"` @@ -61,7 +61,7 @@ type resourceTypes struct { } type svc struct { - data *discoveryData + data *DiscoveryData } func (c *config) ApplyDefaults() { @@ -85,9 +85,9 @@ func (c *config) ApplyDefaults() { } } -func (c *config) prepare() *discoveryData { +func (c *config) prepare() *DiscoveryData { // generates the (static) data structure to be exposed by /ocm-provider - d := &discoveryData{} + d := &DiscoveryData{} if c.Endpoint == "" { d.Enabled = false d.Endpoint = "" diff --git a/pkg/ocm/provider/authorizer/json/json.go b/pkg/ocm/provider/authorizer/json/json.go index e13a038fea..6ff81c494b 100644 --- a/pkg/ocm/provider/authorizer/json/json.go +++ b/pkg/ocm/provider/authorizer/json/json.go @@ -28,7 +28,6 @@ import ( "sync" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/ocm/provider" "github.com/cs3org/reva/pkg/ocm/provider/authorizer/registry" @@ -113,7 +112,6 @@ func (a *authorizer) GetInfoByDomain(ctx context.Context, domain string) (*ocmpr } func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.ProviderInfo) error { - log := appctx.GetLogger(ctx) var err error normalizedDomain, err := normalizeDomain(pi.Domain) if err != nil { @@ -142,7 +140,6 @@ func (a *authorizer) IsProviderAllowed(ctx context.Context, pi *ocmprovider.Prov var ocmHost string for _, p := range a.providers { - log.Debug().Msgf("Comparing '%s' to '%s'", p.Domain, normalizedDomain) if p.Domain == normalizedDomain { ocmHost, err = a.getOCMHost(p) if err != nil { diff --git a/pkg/ocm/share/utils.go b/pkg/ocm/share/utils.go index 63bed64b85..c1ada66f56 100644 --- a/pkg/ocm/share/utils.go +++ b/pkg/ocm/share/utils.go @@ -25,12 +25,12 @@ import ( ) // NewWebDAVProtocol is an abstraction for creating a WebDAV protocol. -func NewWebDAVProtocol(uri, shareSecred string, perms *ocm.SharePermissions) *ocm.Protocol { +func NewWebDAVProtocol(uri, sharedSecret string, perms *ocm.SharePermissions) *ocm.Protocol { return &ocm.Protocol{ Term: &ocm.Protocol_WebdavOptions{ WebdavOptions: &ocm.WebDAVProtocol{ Uri: uri, - SharedSecret: shareSecred, + SharedSecret: sharedSecret, Permissions: perms, }, },