Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog/unreleased/add-ocm-wayf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Enhancement: Add OCM Where Are You From capability

Implements WAYF specific discovery endpoints for the ScienceMesh package,
enabling dynamic OCM provider discovery and federation management.
The implementation follows the OCM Discovery 1.2 specification.

https://github.com/owncloud/reva/pull/432
5 changes: 5 additions & 0 deletions changelog/unreleased/fix-ocm-weddav.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Bugfix: Fix the OCM WebDAV protocol entity mismatch

Fixed the OCM WebDAV protocol entity mismatch

https://github.com/owncloud/reva/pull/425
29 changes: 29 additions & 0 deletions examples/ocmd/federations.demo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
[
{
"federation": "EOSC Federation",
"servers": [
{
"displayName": "CERNBox",
"url": "https://qa.cernbox.cern.ch"
},
{
"displayName": "ownCloud Demo",
"url": "https://oc.example.org"
}
]
},
{
"federation": "Azadeh Afzar Federation",
"servers": [
{
"displayName": "Tehran Center",
"url": "https://1.ocis.cloud.test.azadehafzar.io"
},
{
"displayName": "Mahdi Home",
"url": "https://2.ocis.cloud.test.azadehafzar.io"
}
]
}
]

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

replace github.com/studio-b12/gowebdav => github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202
replace github.com/studio-b12/gowebdav => github.com/kobergj/gowebdav v0.0.0-20251030165916-532350997dde

replace github.com/go-micro/plugins/v4/store/nats-js-kv => github.com/kobergj/plugins/v4/store/nats-js-kv v0.0.0-20240807130109-f62bb67e8c90

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -420,8 +420,8 @@ github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/kobergj/go-micro/v4 v4.0.0-20250117084952-d07d30666b7c h1:21N6rhk5dzUxYhJYiBsyqoRrIowpetooAS5mDwHayeE=
github.com/kobergj/go-micro/v4 v4.0.0-20250117084952-d07d30666b7c/go.mod h1:eE/tD53n3KbVrzrWxKLxdkGw45Fg1qaNLWjpJMvIUF4=
github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202 h1:A1xJ2NKgiYFiaHiLl9B5yw/gUBACSs9crDykTS3GuQI=
github.com/kobergj/gowebdav v0.0.0-20250102091030-aa65266db202/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/kobergj/gowebdav v0.0.0-20251030165916-532350997dde h1:HYcp4J4xYe2m9KSUVbTccJb14TpSs+ldCfDFgqsXedI=
github.com/kobergj/gowebdav v0.0.0-20251030165916-532350997dde/go.mod h1:bHA7t77X/QFExdeAnDzK6vKM34kEZAcE1OX4MfiwjkE=
github.com/kobergj/plugins/v4/store/nats-js-kv v0.0.0-20240807130109-f62bb67e8c90 h1:pfI8Z5yavO6fU6vDGlWhZ4BgDlvj8c6xB7J57HfTPwA=
github.com/kobergj/plugins/v4/store/nats-js-kv v0.0.0-20240807130109-f62bb67e8c90/go.mod h1:pjcozWijkNPbEtX5SIQaxEW/h8VAVZYTLx+70bmB3LY=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
Expand Down
4 changes: 2 additions & 2 deletions internal/grpc/services/ocmcore/ocmcore.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCM
}
fileMask := &fieldmaskpb.FieldMask{Paths: []string{"protocols"}}

user := &userpb.User{Id: ocmuser.RemoteID(&userpb.UserId{OpaqueId: grantee})}
user := &userpb.User{Id: ocmuser.DecodeRemoteUserFederatedID(&userpb.UserId{OpaqueId: grantee})}
_, err := s.repo.UpdateReceivedShare(ctx, user, &ocm.ReceivedShare{
Id: &ocm.ShareId{
OpaqueId: req.GetOcmShareId(),
Expand All @@ -185,7 +185,7 @@ func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCM
return nil, errtypes.UserRequired("missing remote user id in a metadata")
}

user := &userpb.User{Id: ocmuser.RemoteID(&userpb.UserId{OpaqueId: grantee})}
user := &userpb.User{Id: ocmuser.DecodeRemoteUserFederatedID(&userpb.UserId{OpaqueId: grantee})}

err := s.repo.DeleteReceivedShare(ctx, user, &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Expand Down
9 changes: 7 additions & 2 deletions internal/grpc/services/ocminvitemanager/ocminvitemanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,8 +180,8 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite
remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(),
RecipientProvider: s.conf.ProviderDomain,
// The UserID is only a string here. To not loose the IDP information we use the FederatedID encoding
// i.e. base64(UserID@IDP)
// The UserID is only a string here. To not lose the IDP information we use the FederatedID encoding
// i.e. UserID@IDP
UserID: ocmuser.FederatedID(user.GetId(), "").GetOpaqueId(),
Email: user.GetMail(),
Name: user.GetDisplayName(),
Expand Down Expand Up @@ -223,6 +223,9 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite
OpaqueId: remoteUser.UserID,
}

// we need to use a unique identifier for federated users
// remoteUserID = ocmuser.FederatedID(remoteUserID, "")

if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{
Id: remoteUserID,
Mail: remoteUser.Email,
Expand Down Expand Up @@ -282,6 +285,8 @@ func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRe
}

remoteUser := req.GetRemoteUser()
// we need to use a unique identifier for federated users
//remoteUser.Id = ocmuser.FederatedID(remoteUser.Id, "")

if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), remoteUser); err != nil {
if !errors.Is(err, invite.ErrUserAlreadyAccepted) {
Expand Down
6 changes: 3 additions & 3 deletions internal/grpc/services/ocmshareprovider/ocmshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *oc

return &ocmd.WebDAV{
Permissions: perms,
URL: s.webdavURL(ctx, share),
URI: s.webdavURL(ctx, share),
SharedSecret: share.Token,
}
}
Expand Down Expand Up @@ -325,9 +325,9 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq

// 2.b replace outgoing user ids with ocm user ids
// unpack the federated user id
shareWith := ocmuser.FormatOCMUser(ocmuser.RemoteID(req.GetGrantee().GetUserId()))
shareWith := ocmuser.FormatOCMUser(ocmuser.FederatedID(req.GetGrantee().GetUserId(), ""))

// wrap the local user id in a federated user id
// wrap the local user id in a local federated user id
owner := ocmuser.FormatOCMUser(ocmuser.FederatedID(info.Owner, s.conf.ProviderDomain))
sender := ocmuser.FormatOCMUser(ocmuser.FederatedID(user.Id, s.conf.ProviderDomain))

Expand Down
142 changes: 142 additions & 0 deletions internal/http/services/ocmd/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// Copyright 2018-2025 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 ocmd

import (
"context"
"crypto/tls"
"encoding/json"
"io"
"net/http"
"net/url"
"time"

"github.com/cs3org/reva/v2/internal/http/services/wellknown"
"github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/pkg/errors"
)

// OCMClient is the client for an OCM provider.
type OCMClient struct {
client *http.Client
}

// NewClient returns a new OCMClient.
func NewClient(timeout time.Duration, insecure bool) *OCMClient {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: insecure},
}
return &OCMClient{
client: &http.Client{
Transport: tr,
Timeout: timeout,
},
}
}

// Discover returns the OCM discovery information for a remote endpoint.
// It tries /.well-known/ocm first, then falls back to /ocm-provider (legacy).
// 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) {
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.InternalError("Invalid response on OCM discovery")
}
}

var disco wellknown.OcmDiscoveryData
err = json.Unmarshal(body, &disco)
if err != nil {
log.Warn().Err(err).Str("sender", remoteurl).Str("response", string(body)).
Msg("malformed response")
return nil, errtypes.InternalError("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 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 OCM discovery request")
}
defer func(body io.ReadCloser) {
err := body.Close()
if err != nil {
log.Warn().Err(err).Msg("error closing response body")
}
}(resp.Body)
if resp.StatusCode != http.StatusOK {
log.Warn().Str("sender", url).Int("status", resp.StatusCode).Msg("discovery returned")
return nil, errtypes.NewErrtypeFromHTTPStatusCode(resp.StatusCode, "Remote does not offer a valid OCM discovery endpoint")
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrap(err, "malformed remote OCM discovery")
}
return body, nil
}

// GetDirectoryService fetches a directory service listing from the given URL per OCM spec Appendix C.
func (c *OCMClient) GetDirectoryService(ctx context.Context, directoryURL string) (*DirectoryService, error) {
log := appctx.GetLogger(ctx)

// TODO(@MahdiBaghbani): the discover() should be changed into a generic function that can be used to fetch any OCM endpoint. I'll do it in the security PR to minimize conflicts.
body, err := c.discover(ctx, directoryURL)
if err != nil {
return nil, errors.Wrap(err, "error fetching directory service")
}

var dirService DirectoryService
if err := json.Unmarshal(body, &dirService); err != nil {
log.Warn().Err(err).Str("url", directoryURL).Str("response", string(body)).Msg("malformed directory service response")
return nil, errors.Wrap(err, "invalid directory service payload")
}

// Validate required fields
if dirService.Federation == "" {
return nil, errtypes.InternalError("directory service missing required 'federation' field")
}
// Servers can be empty array, that's valid

log.Debug().Str("url", directoryURL).Str("federation", dirService.Federation).Int("servers", len(dirService.Servers)).Msg("fetched directory service")
return &dirService, nil
}
4 changes: 2 additions & 2 deletions internal/http/services/ocmd/invites.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ type acceptInviteRequest struct {
Email string `json:"email"`
}

// AcceptInvite informs avout an accepted invitation so that the users
// AcceptInvite informs about an accepted invitation so that the users
// can initiate the OCM share creation.
func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
Expand All @@ -73,7 +73,7 @@ func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) {
}

if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil)
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipientProvider must not be null", nil)
return
}

Expand Down
34 changes: 32 additions & 2 deletions internal/http/services/ocmd/protocols.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,37 @@ type Protocol interface {
type WebDAV struct {
SharedSecret string `json:"sharedSecret" validate:"required"`
Permissions []string `json:"permissions" validate:"required,dive,required,oneof=read write share"`
URL string `json:"url" validate:"required"`
URI string `json:"uri" validate:"required"`
}

// UnmarshalJSON implements custom JSON unmarshaling for backward compatibility.
// It supports both "url" (legacy) and "uri" (new) field names.
func (w *WebDAV) UnmarshalJSON(data []byte) error {
// Define a temporary struct with both url and uri fields
type WebDAVAlias struct {
SharedSecret string `json:"sharedSecret"`
Permissions []string `json:"permissions"`
URL string `json:"url"`
URI string `json:"uri"`
}

var alias WebDAVAlias
if err := json.Unmarshal(data, &alias); err != nil {
return err
}

// Copy common fields
w.SharedSecret = alias.SharedSecret
w.Permissions = alias.Permissions

// Use URI if present, otherwise fall back to URL for backward compatibility
if alias.URI != "" {
w.URI = alias.URI
} else {
w.URI = alias.URL
}

return nil
}

// ToOCMProtocol convert the protocol to a ocm Protocol struct.
Expand All @@ -73,7 +103,7 @@ func (w *WebDAV) ToOCMProtocol() *ocm.Protocol {
}
}

return ocmshare.NewWebDAVProtocol(w.URL, w.SharedSecret, perms)
return ocmshare.NewWebDAVProtocol(w.URI, w.SharedSecret, perms)
}

// Webapp contains the parameters for the Webapp protocol.
Expand Down
Loading