1
0
mirror of https://github.com/owncloud/ocis.git synced 2025-04-18 23:44:07 +03:00

Add the ocm notification handler

This commit is contained in:
Roman Perekhod 2025-02-17 14:50:23 +01:00
parent ceca4111ec
commit d828e82b7c
16 changed files with 376 additions and 29 deletions

2
.gitignore vendored
View File

@ -38,7 +38,7 @@ vendor-php
# API acceptance tests - auto-generated files # API acceptance tests - auto-generated files
.php-cs-fixer.cache .php-cs-fixer.cache
# QA activity reports # QA activity reports
tests/qa-activity-report/reports/ tests/qa-activity-report/reports/
# drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml # drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml

View File

@ -0,0 +1,6 @@
Enhancement: Add the ocm notification handler
Added the ocm notification handler that allows receiving a notification from a remote party about changes to a previously known entity.
https://github.com/owncloud/ocis/pull/11005
https://github.com/owncloud/enterprise/issues/7075

2
go.mod
View File

@ -17,7 +17,7 @@ require (
github.com/cenkalti/backoff v2.2.1+incompatible github.com/cenkalti/backoff v2.2.1+incompatible
github.com/coreos/go-oidc/v3 v3.11.0 github.com/coreos/go-oidc/v3 v3.11.0
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1 github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1
github.com/cs3org/reva/v2 v2.27.5-0.20250205144957-2a7145a1d4ad github.com/cs3org/reva/v2 v2.27.5-0.20250217133727-8aefc9e791f7
github.com/davidbyttow/govips/v2 v2.16.0 github.com/davidbyttow/govips/v2 v2.16.0
github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8 github.com/dhowden/tag v0.0.0-20240417053706-3d75831295e8
github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e

4
go.sum
View File

@ -251,8 +251,8 @@ github.com/crewjam/saml v0.4.14 h1:g9FBNx62osKusnFzs3QTN5L9CVA/Egfgm+stJShzw/c=
github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME= github.com/crewjam/saml v0.4.14/go.mod h1:UVSZCf18jJkk6GpWNVqcyQJMD5HsRugBPf4I1nl2mME=
github.com/cs3org/go-cs3apis v0.0.0-20241105092511-3ad35d174fc1 h1:RU6LT6mkD16xZs011+8foU7T3LrPvTTSWeTQ9OgfhkA= 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-20241105092511-3ad35d174fc1/go.mod h1:DedpcqXl193qF/08Y04IO0PpxyyMu8+GrkD6kWK2MEQ=
github.com/cs3org/reva/v2 v2.27.5-0.20250205144957-2a7145a1d4ad h1:Ae/greWkZD/skjC70ZCxDVnABxga7sqWD6GpGbDzwg0= github.com/cs3org/reva/v2 v2.27.5-0.20250217133727-8aefc9e791f7 h1:slkg8L8rG4ei0OKwEMDZ7EY88d3cduzO53rWMY6lX+A=
github.com/cs3org/reva/v2 v2.27.5-0.20250205144957-2a7145a1d4ad/go.mod h1:8yvuebW1eCKzRfzNXy+RSYYEESmMp5mCuM6MQ47zJow= github.com/cs3org/reva/v2 v2.27.5-0.20250217133727-8aefc9e791f7/go.mod h1:1H26PMXoa1rDrIoZ7lGOerq1Bg07/5srYfRaKfxBSsc=
github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4=
github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg=
github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=

View File

@ -69,7 +69,7 @@ func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCore
res, err := c.DeleteOCMCoreShare(ctx, req) res, err := c.DeleteOCMCoreShare(ctx, req)
if err != nil { if err != nil {
return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare") return nil, errors.Wrap(err, "gateway: error calling DeleteOCMCoreShare")
} }
return res, nil return res, nil

View File

@ -20,9 +20,11 @@ package ocmcore
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"time" "time"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@ -30,8 +32,10 @@ import (
"github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/share" "github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
ocmuser "github.com/cs3org/reva/v2/pkg/ocm/user"
"github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc"
"github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/rgrpc/status"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/cs3org/reva/v2/pkg/utils/cfg" "github.com/cs3org/reva/v2/pkg/utils/cfg"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -93,7 +97,11 @@ func (s *service) Close() error {
} }
func (s *service) UnprotectedEndpoints() []string { func (s *service) UnprotectedEndpoints() []string {
return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"} return []string{
ocmcore.OcmCoreAPI_CreateOCMCoreShare_FullMethodName,
ocmcore.OcmCoreAPI_UpdateOCMCoreShare_FullMethodName,
ocmcore.OcmCoreAPI_DeleteOCMCoreShare_FullMethodName,
}
} }
// CreateOCMCoreShare is called when an OCM request comes into this reva instance from. // CreateOCMCoreShare is called when an OCM request comes into this reva instance from.
@ -144,5 +152,30 @@ func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCM
} }
func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) { func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) {
return nil, errtypes.NotSupported("not implemented") grantee := utils.ReadPlainFromOpaque(req.GetOpaque(), "grantee")
if grantee == "" {
return nil, errtypes.UserRequired("missing remote user id in a metadata")
}
user := &userpb.User{Id: ocmuser.RemoteID(&userpb.UserId{OpaqueId: grantee})}
err := s.repo.DeleteReceivedShare(ctx, user, &ocm.ShareReference{
Spec: &ocm.ShareReference_Id{
Id: &ocm.ShareId{
OpaqueId: req.GetId(),
},
},
})
res := &ocmcore.DeleteOCMCoreShareResponse{}
if err == nil {
res.Status = status.NewOK(ctx)
} else {
var notFound errtypes.NotFound
if errors.As(err, &notFound) {
res.Status = status.NewNotFound(ctx, "remote ocm share not found")
} else {
res.Status = status.NewInternal(ctx, "error deleting remote ocm share")
}
}
return res, nil
} }

View File

@ -170,6 +170,12 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite
return nil, err return nil, err
} }
if req.GetOriginSystemProvider().Domain == s.conf.ProviderDomain {
return &invitepb.ForwardInviteResponse{
Status: status.NewInvalid(ctx, "can not accept an invite from the same instance"),
}, nil
}
// Accept the invitation on the remote OCM provider // Accept the invitation on the remote OCM provider
remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{ remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{
Token: req.InviteToken.GetToken(), Token: req.InviteToken.GetToken(),

View File

@ -38,6 +38,7 @@ import (
ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" ctxpkg "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/client" "github.com/cs3org/reva/v2/pkg/ocm/client"
"github.com/cs3org/reva/v2/pkg/ocm/payload"
"github.com/cs3org/reva/v2/pkg/ocm/share" "github.com/cs3org/reva/v2/pkg/ocm/share"
"github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry"
ocmuser "github.com/cs3org/reva/v2/pkg/ocm/user" ocmuser "github.com/cs3org/reva/v2/pkg/ocm/user"
@ -379,9 +380,22 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq
} }
func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) { func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) {
// TODO (gdelmont): notify the remote provider using the /notification ocm endpoint
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
user := ctxpkg.ContextMustGetUser(ctx) user := ctxpkg.ContextMustGetUser(ctx)
getShareRes, err := s.GetOCMShare(ctx, &ocm.GetOCMShareRequest{
Ref: req.Ref,
})
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error getting ocm share"),
}, nil
}
if getShareRes.Status.Code != rpc.Code_CODE_OK {
res := &ocm.RemoveOCMShareResponse{
Status: getShareRes.GetStatus(),
}
return res, nil
}
if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil { if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil {
if errors.Is(err, share.ErrShareNotFound) { if errors.Is(err, share.ErrShareNotFound) {
return &ocm.RemoveOCMShareResponse{ return &ocm.RemoveOCMShareResponse{
@ -389,10 +403,54 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq
}, nil }, nil
} }
return &ocm.RemoveOCMShareResponse{ return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error removing share"), Status: status.NewInternal(ctx, "error deleting share"),
}, nil }, nil
} }
// TODO: We should not fail the whole operation if the notification fails
gatewayClient, err := s.gatewaySelector.Next()
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error getting gateway client"),
}, nil
}
providerInfoResp, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{
Domain: getShareRes.GetShare().GetGrantee().GetUserId().GetIdp(),
})
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "error getting provider info"),
}, nil
}
if providerInfoResp.Status.Code != rpc.Code_CODE_OK {
return &ocm.RemoveOCMShareResponse{
Status: providerInfoResp.Status,
}, nil
}
ocmEndpoint, err := getOCMEndpoint(providerInfoResp.GetProviderInfo())
if err != nil {
return &ocm.RemoveOCMShareResponse{
Status: status.NewInternal(ctx, "the selected provider does not have an OCM endpoint"),
}, nil
}
newShareReq := &payload.NotificationRequest{
NotificationType: payload.SHARE_UNSHARED,
ResourceType: "file", // use type "file" for shared files or folders
ProviderId: getShareRes.GetShare().GetId().GetOpaqueId(),
Notification: &payload.Notification{
Grantee: getShareRes.GetShare().GetGrantee().GetUserId().GetOpaqueId(),
},
}
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
err = s.client.NotifyRemote(ctx, ocmEndpoint, newShareReq)
if err != nil {
// Continue even if the notification fails
appctx.GetLogger(ctx).Err(err).Msg("error notifying ocm remote provider")
}
return &ocm.RemoveOCMShareResponse{ return &ocm.RemoveOCMShareResponse{
Status: status.NewOK(ctx), Status: status.NewOK(ctx),
}, nil }, nil

View File

@ -19,20 +19,36 @@
package ocmd package ocmd
import ( import (
"io" "context"
"encoding/json"
"fmt"
"mime" "mime"
"net/http" "net/http"
"github.com/cs3org/reva/v2/internal/http/services/reqres" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/ocm/payload"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
"github.com/cs3org/reva/v2/pkg/utils"
"github.com/go-chi/render"
) )
// var validate = validator.New() // var validate = validator.New()
type notifHandler struct { type notifHandler struct {
gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
} }
func (h *notifHandler) init(c *config) error { func (h *notifHandler) init(c *config) error {
gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
if err != nil {
return err
}
h.gatewaySelector = gatewaySelector
return nil return nil
} }
@ -42,25 +58,100 @@ func (h *notifHandler) init(c *config) error {
func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) { func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
log := appctx.GetLogger(ctx) log := appctx.GetLogger(ctx)
req, err := getNotification(r) req, err := getNotification(w, r)
if err != nil { if err != nil {
reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) renderErrorBadRequest(w, r, http.StatusBadRequest, err.Error())
return return
} }
// TODO(lopresti) this is all to be implemented. For now we just log what we got // TODO(lopresti) this is all to be implemented. For now we just log what we got
log.Debug().Msgf("Received OCM notification: %+v", req) log.Debug().Msgf("Received OCM notification: %+v", req)
// this is to please Nextcloud var status *rpc.Status
w.WriteHeader(http.StatusCreated) switch req.NotificationType {
case payload.SHARE_UNSHARED:
if req.Notification.Grantee == "" {
renderErrorBadRequest(w, r, http.StatusBadRequest, "grantee is required")
}
status, err = h.handleShareUnshared(ctx, req)
if err != nil {
log.Err(err).Any("NotificationRequest", req).Msg("error getting gateway client")
renderErrorBadRequest(w, r, http.StatusInternalServerError, status.GetMessage())
}
case payload.SHARE_CHANGE_PERMISSION:
// TODO implement the SHARE_CHANGE_PERMISSION
w.WriteHeader(http.StatusNotImplemented)
return
default:
renderErrorBadRequest(w, r, http.StatusBadRequest, "NotificationType "+req.NotificationType+" is not supported")
return
}
// parse the response status
switch status.GetCode() {
case rpc.Code_CODE_OK:
w.WriteHeader(http.StatusCreated)
return
case rpc.Code_CODE_INVALID_ARGUMENT:
renderErrorBadRequest(w, r, http.StatusBadRequest, status.GetMessage())
return
case rpc.Code_CODE_UNAUTHENTICATED:
w.WriteHeader(http.StatusUnauthorized)
return
case rpc.Code_CODE_PERMISSION_DENIED:
w.WriteHeader(http.StatusForbidden)
return
default:
log.Error().Str("code", status.GetCode().String()).Str("message", status.GetMessage()).Str("NotificationType", req.NotificationType).Msg("error handling notification")
w.WriteHeader(http.StatusInternalServerError)
}
} }
func getNotification(r *http.Request) (string, error) { func (h *notifHandler) handleShareUnshared(ctx context.Context, req *payload.NotificationRequest) (*rpc.Status, error) {
// var req notificationRequest gatewayClient, err := h.gatewaySelector.Next()
if err != nil {
return nil, fmt.Errorf("error getting gateway client: %w", err)
}
o := &typesv1beta1.Opaque{}
utils.AppendPlainToOpaque(o, "grantee", req.Notification.Grantee)
res, err := gatewayClient.DeleteOCMCoreShare(ctx, &ocmcore.DeleteOCMCoreShareRequest{
Id: req.ProviderId,
Opaque: o,
})
if err != nil {
return nil, fmt.Errorf("error calling DeleteOCMCoreShare: %w", err)
}
return res.GetStatus(), nil
}
func getNotification(w http.ResponseWriter, r *http.Request) (*payload.NotificationRequest, error) {
contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
if err == nil && contentType == "application/json" { if err == nil && contentType == "application/json" {
bytes, _ := io.ReadAll(r.Body) n := &payload.NotificationRequest{}
return string(bytes), nil err := json.NewDecoder(r.Body).Decode(&n)
if err != nil {
return nil, err
}
return n, nil
} }
return "", nil return nil, err
}
func renderJSON(w http.ResponseWriter, r *http.Request, statusCode int, resp any) {
render.Status(r, statusCode)
render.JSON(w, r, resp)
}
func renderErrorBadRequest(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
resp := &payload.ErrorMessageResponse{
Message: "BAD_REQUEST",
ValidationErrors: []*payload.ValidationError{
{
Name: "Notification",
Message: message,
},
},
}
renderJSON(w, r, http.StatusBadRequest, resp)
} }

View File

@ -27,15 +27,13 @@ import (
"strings" "strings"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" 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"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/internal/http/services/reqres" "github.com/cs3org/reva/v2/internal/http/services/reqres"
"github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
@ -78,7 +76,7 @@ type createShareRequest struct {
Protocols Protocols `json:"protocol" validate:"required"` Protocols Protocols `json:"protocol" validate:"required"`
} }
// CreateShare sends all the informations to the consumer needed to start // CreateShare sends all the information to the consumer needed to start
// synchronization between the two services. // synchronization between the two services.
func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx := r.Context()
@ -180,7 +178,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) {
return return
} }
if userRes.Status.Code != rpc.Code_CODE_OK { if createShareResp.Status.Code != rpc.Code_CODE_OK {
// TODO: define errors in the cs3apis // TODO: define errors in the cs3apis
reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message)) reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message))
return return

View File

@ -22,14 +22,17 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"strconv"
"time" "time"
"github.com/cs3org/reva/v2/internal/http/services/ocmd" "github.com/cs3org/reva/v2/internal/http/services/ocmd"
"github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/appctx"
"github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/errtypes"
"github.com/cs3org/reva/v2/pkg/ocm/payload"
"github.com/cs3org/reva/v2/pkg/rhttp" "github.com/cs3org/reva/v2/pkg/rhttp"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -278,3 +281,68 @@ func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabiliti
return &cap, nil return &cap, nil
} }
// NotifyRemote sends a notification to a remote OCM instance.
// Send a notification to a remote party about a previously known entity
// Notifications are optional messages. They are expected to be used to inform the other party about a change about a previously known entity,
// such as a share or a trusted user. For example, a notification MAY be sent by a recipient to let the provider know that
// the recipient declined a share. In this case, the provider site MAY mark the share as declined for its user(s). Similarly,
// it MAY be sent by a provider to let the recipient know that the provider removed a given share, such that the recipient MAY clean it up from its database.
// A notification MAY also be sent to let a recipient know that the provider removed that recipient from the list of trusted users, along with any related share.
// The recipient MAY reciprocally remove that provider from the list of trusted users, along with any related share.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
func (c *OCMClient) NotifyRemote(ctx context.Context, endpoint string, r *payload.NotificationRequest) error {
url, err := url.JoinPath(endpoint, "notifications")
if err != nil {
return err
}
body, err := r.ToJSON()
if err != nil {
return err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, body)
if err != nil {
return errors.Wrap(err, "error creating request")
}
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return errors.Wrap(err, "error doing request")
}
defer resp.Body.Close()
err = c.parseNotifyRemoteResponse(resp, nil)
if err != nil {
appctx.GetLogger(ctx).Err(err).Msg("error notifying remote OCM instance")
return err
}
return nil
}
func (c *OCMClient) parseNotifyRemoteResponse(r *http.Response, resp any) error {
var err error
switch r.StatusCode {
case http.StatusOK, http.StatusCreated:
if resp == nil {
return nil
}
err := json.NewDecoder(r.Body).Decode(&resp)
if err != nil {
return errors.Wrap(err, fmt.Sprintf("http status code: %v, error decoding response body", r.StatusCode))
}
return nil
case http.StatusBadRequest:
err = ErrInvalidParameters
case http.StatusUnauthorized, http.StatusForbidden:
err = ErrServiceNotTrusted
default:
err = errtypes.InternalError("request finished whit code " + strconv.Itoa(r.StatusCode))
}
body, err2 := io.ReadAll(r.Body)
if err2 != nil {
return errors.Wrap(err, "error reading response body "+err2.Error())
}
return errors.Wrap(err, string(body))
}

View File

@ -0,0 +1,54 @@
package payload
import (
"bytes"
"encoding/json"
"io"
)
const (
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
// NotificationType one of "SHARE_ACCEPTED", "SHARE_DECLINED", "SHARE_CHANGE_PERMISSION", "SHARE_UNSHARED", "USER_REMOVED"
SHARE_UNSHARED = "SHARE_UNSHARED"
SHARE_CHANGE_PERMISSION = "SHARE_CHANGE_PERMISSION"
)
// NotificationRequest is the request payload for the OCM API notifications endpoint.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
type NotificationRequest struct {
NotificationType string `json:"notificationType" validate:"required"`
ResourceType string `json:"resourceType" validate:"required"`
// Identifier to identify the shared resource at the provider side. This is unique per provider such that if the same resource is shared twice, this providerId will not be repeated.
ProviderId string `json:"providerId" validate:"required"`
// Optional additional parameters, depending on the notification and the resource type.
Notification *Notification `json:"notification,omitempty"`
}
// Notification is the payload for the notification field in the NotificationRequest.
type Notification struct {
// Owner string `json:"owner,omitempty"`
Grantee string `json:"grantee,omitempty"`
SharedSecret string `json:"sharedSecret,omitempty"`
}
// ToJSON returns the JSON io.Reader of the NotificationRequest.
func (r *NotificationRequest) ToJSON() (io.Reader, error) {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(r); err != nil {
return nil, err
}
return &b, nil
}
// ErrorMessageResponse is the response returned by the OCM API in case of an error.
// https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
type ErrorMessageResponse struct {
Message string `json:"message"`
ValidationErrors []*ValidationError `json:"validationErrors,omitempty"`
}
// ValidationError is the payload for the validationErrors field in the ErrorMessageResponse.
type ValidationError struct {
Name string `json:"name"`
Message string `json:"message"`
}

View File

@ -379,6 +379,12 @@ func receivedShareEqual(ref *ocm.ShareReference, s *ocm.ReceivedShare) bool {
return true return true
} }
} }
// Match the reserved share by the remote share id
if ref.GetId() != nil && s.RemoteShareId != "" {
if ref.GetId().GetOpaqueId() == s.RemoteShareId {
return true
}
}
return false return false
} }
@ -589,6 +595,23 @@ func (m *mgr) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.
return nil, errtypes.NotFound(ref.String()) return nil, errtypes.NotFound(ref.String())
} }
func (m *mgr) DeleteReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
m.Lock()
defer m.Unlock()
if err := m.load(); err != nil {
return err
}
for id, share := range m.model.ReceivedShares {
if receivedShareEqual(ref, share) && utils.UserEqual(user.Id, share.GetGrantee().GetUserId()) {
delete(m.model.ReceivedShares, id)
return m.save()
}
}
return errtypes.NotFound(ref.String())
}
func (m *mgr) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) { func (m *mgr) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
rs, err := m.GetReceivedShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Id}}) rs, err := m.GetReceivedShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Id}})
if err != nil { if err != nil {

View File

@ -28,6 +28,7 @@ import (
"strings" "strings"
userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/cs3org/reva/v2/pkg/errtypes"
ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1"
provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1"
@ -345,6 +346,11 @@ func (sm *Manager) GetReceivedShare(ctx context.Context, user *userpb.User, ref
}, nil }, nil
} }
// DeleteReceivedShare deletes the share pointed by ref.
func (sm *Manager) DeleteReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error {
return errtypes.NotSupported("not implemented")
}
// UpdateReceivedShare updates the received share with share state. // UpdateReceivedShare updates the received share with share state.
func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) { func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) {
type paramsObj struct { type paramsObj struct {

View File

@ -55,6 +55,9 @@ type Repository interface {
// GetReceivedShare returns the information for a received share the user has access. // GetReceivedShare returns the information for a received share the user has access.
GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) (*ocm.ReceivedShare, error)
// DeleteReceivedShare deletes the share pointed by ref.
DeleteReceivedShare(ctx context.Context, user *userpb.User, ref *ocm.ShareReference) error
// UpdateReceivedShare updates the received share with share state. // UpdateReceivedShare updates the received share with share state.
UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error)
} }

3
vendor/modules.txt vendored
View File

@ -367,7 +367,7 @@ github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1
github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1 github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1
github.com/cs3org/go-cs3apis/cs3/tx/v1beta1 github.com/cs3org/go-cs3apis/cs3/tx/v1beta1
github.com/cs3org/go-cs3apis/cs3/types/v1beta1 github.com/cs3org/go-cs3apis/cs3/types/v1beta1
# github.com/cs3org/reva/v2 v2.27.5-0.20250205144957-2a7145a1d4ad # github.com/cs3org/reva/v2 v2.27.5-0.20250217133727-8aefc9e791f7
## explicit; go 1.22.7 ## explicit; go 1.22.7
github.com/cs3org/reva/v2/cmd/revad/internal/grace github.com/cs3org/reva/v2/cmd/revad/internal/grace
github.com/cs3org/reva/v2/cmd/revad/runtime github.com/cs3org/reva/v2/cmd/revad/runtime
@ -558,6 +558,7 @@ github.com/cs3org/reva/v2/pkg/ocm/invite/repository/loader
github.com/cs3org/reva/v2/pkg/ocm/invite/repository/memory github.com/cs3org/reva/v2/pkg/ocm/invite/repository/memory
github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry
github.com/cs3org/reva/v2/pkg/ocm/invite/repository/sql github.com/cs3org/reva/v2/pkg/ocm/invite/repository/sql
github.com/cs3org/reva/v2/pkg/ocm/payload
github.com/cs3org/reva/v2/pkg/ocm/provider github.com/cs3org/reva/v2/pkg/ocm/provider
github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/json github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/json
github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/loader github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/loader