From d828e82b7cc4ff54bd4b4845f032b4d806de1c6b Mon Sep 17 00:00:00 2001 From: Roman Perekhod Date: Mon, 17 Feb 2025 14:50:23 +0100 Subject: [PATCH] Add the ocm notification handler --- .gitignore | 2 +- .../unreleased/add-ocm-notificatin-handler.md | 6 + go.mod | 2 +- go.sum | 4 +- .../internal/grpc/services/gateway/ocmcore.go | 2 +- .../internal/grpc/services/ocmcore/ocmcore.go | 37 +++++- .../ocminvitemanager/ocminvitemanager.go | 6 + .../ocmshareprovider/ocmshareprovider.go | 64 +++++++++- .../http/services/ocmd/notifications.go | 113 ++++++++++++++++-- .../v2/internal/http/services/ocmd/shares.go | 12 +- .../cs3org/reva/v2/pkg/ocm/client/client.go | 68 +++++++++++ .../cs3org/reva/v2/pkg/ocm/payload/payload.go | 54 +++++++++ .../v2/pkg/ocm/share/repository/json/json.go | 23 ++++ .../share/repository/nextcloud/nextcloud.go | 6 + .../cs3org/reva/v2/pkg/ocm/share/share.go | 3 + vendor/modules.txt | 3 +- 16 files changed, 376 insertions(+), 29 deletions(-) create mode 100644 changelog/unreleased/add-ocm-notificatin-handler.md create mode 100644 vendor/github.com/cs3org/reva/v2/pkg/ocm/payload/payload.go diff --git a/.gitignore b/.gitignore index 26dde1619f4..180df74db5f 100644 --- a/.gitignore +++ b/.gitignore @@ -38,7 +38,7 @@ vendor-php # API acceptance tests - auto-generated files .php-cs-fixer.cache -# QA activity reports +# QA activity reports tests/qa-activity-report/reports/ # drone CI is in .drone.star, do not let someone accidentally commit a local .drone.yml diff --git a/changelog/unreleased/add-ocm-notificatin-handler.md b/changelog/unreleased/add-ocm-notificatin-handler.md new file mode 100644 index 00000000000..9b536b9c758 --- /dev/null +++ b/changelog/unreleased/add-ocm-notificatin-handler.md @@ -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 diff --git a/go.mod b/go.mod index bf25625440b..ce9e1c90c6c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/cenkalti/backoff v2.2.1+incompatible github.com/coreos/go-oidc/v3 v3.11.0 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/dhowden/tag v0.0.0-20240417053706-3d75831295e8 github.com/dutchcoders/go-clamd v0.0.0-20170520113014-b970184f4d9e diff --git a/go.sum b/go.sum index a2a3ea3b7f8..382af822eaa 100644 --- a/go.sum +++ b/go.sum @@ -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/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/reva/v2 v2.27.5-0.20250205144957-2a7145a1d4ad h1:Ae/greWkZD/skjC70ZCxDVnABxga7sqWD6GpGbDzwg0= -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 h1:slkg8L8rG4ei0OKwEMDZ7EY88d3cduzO53rWMY6lX+A= +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/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= diff --git a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/gateway/ocmcore.go b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/gateway/ocmcore.go index 8c47bdc76b0..8d474e55aec 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/gateway/ocmcore.go +++ b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/gateway/ocmcore.go @@ -69,7 +69,7 @@ func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCore res, err := c.DeleteOCMCoreShare(ctx, req) if err != nil { - return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare") + return nil, errors.Wrap(err, "gateway: error calling DeleteOCMCoreShare") } return res, nil diff --git a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmcore/ocmcore.go b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmcore/ocmcore.go index c9946c3c963..3c88f3db9f9 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmcore/ocmcore.go +++ b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmcore/ocmcore.go @@ -20,9 +20,11 @@ package ocmcore import ( "context" + "errors" "fmt" "time" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/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/ocm/share" "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/status" + "github.com/cs3org/reva/v2/pkg/utils" "github.com/cs3org/reva/v2/pkg/utils/cfg" "github.com/rs/zerolog" "google.golang.org/grpc" @@ -93,7 +97,11 @@ func (s *service) Close() error { } 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. @@ -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) { - 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, ¬Found) { + res.Status = status.NewNotFound(ctx, "remote ocm share not found") + } else { + res.Status = status.NewInternal(ctx, "error deleting remote ocm share") + } + } + return res, nil } diff --git a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go index ec368013343..05f92c91ae4 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go +++ b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocminvitemanager/ocminvitemanager.go @@ -170,6 +170,12 @@ func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInvite 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 remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{ Token: req.InviteToken.GetToken(), diff --git a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index f1b0ed09e1d..4b2b93d6a61 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/vendor/github.com/cs3org/reva/v2/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -38,6 +38,7 @@ import ( ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/errtypes" "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/repository/registry" 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) { - // 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) + 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 errors.Is(err, share.ErrShareNotFound) { return &ocm.RemoveOCMShareResponse{ @@ -389,10 +403,54 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq }, nil } return &ocm.RemoveOCMShareResponse{ - Status: status.NewInternal(ctx, "error removing share"), + Status: status.NewInternal(ctx, "error deleting share"), }, 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{ Status: status.NewOK(ctx), }, nil diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/notifications.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/notifications.go index 5801f3e8fc8..66d556d04b5 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/notifications.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/notifications.go @@ -19,20 +19,36 @@ package ocmd import ( - "io" + "context" + "encoding/json" + "fmt" "mime" "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/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() type notifHandler struct { + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] } func (h *notifHandler) init(c *config) error { + gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc) + if err != nil { + return err + } + h.gatewaySelector = gatewaySelector + return nil } @@ -42,25 +58,100 @@ func (h *notifHandler) init(c *config) error { func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := appctx.GetLogger(ctx) - req, err := getNotification(r) + req, err := getNotification(w, r) if err != nil { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) + renderErrorBadRequest(w, r, http.StatusBadRequest, err.Error()) return } // TODO(lopresti) this is all to be implemented. For now we just log what we got log.Debug().Msgf("Received OCM notification: %+v", req) - // this is to please Nextcloud - w.WriteHeader(http.StatusCreated) + var status *rpc.Status + 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 (h *notifHandler) handleShareUnshared(ctx context.Context, req *payload.NotificationRequest) (*rpc.Status, error) { + 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(r *http.Request) (string, error) { - // var req notificationRequest +func getNotification(w http.ResponseWriter, r *http.Request) (*payload.NotificationRequest, error) { contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) if err == nil && contentType == "application/json" { - bytes, _ := io.ReadAll(r.Body) - return string(bytes), nil + n := &payload.NotificationRequest{} + err := json.NewDecoder(r.Body).Decode(&n) + if err != nil { + return nil, err + } + return n, 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, + }, + }, } - return "", nil + renderJSON(w, r, http.StatusBadRequest, resp) } diff --git a/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/shares.go b/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/shares.go index 1d3c455a56b..cb7de98ae60 100644 --- a/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/shares.go +++ b/vendor/github.com/cs3org/reva/v2/internal/http/services/ocmd/shares.go @@ -27,15 +27,13 @@ import ( "strings" 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" 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" - 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/pkg/appctx" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" @@ -78,7 +76,7 @@ type createShareRequest struct { 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. func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -180,7 +178,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { return } - if userRes.Status.Code != rpc.Code_CODE_OK { + if createShareResp.Status.Code != rpc.Code_CODE_OK { // TODO: define errors in the cs3apis reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message)) return diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/client/client.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/client/client.go index 1e25c09206e..66e1ad76cf4 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/ocm/client/client.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/client/client.go @@ -22,14 +22,17 @@ import ( "bytes" "context" "encoding/json" + "fmt" "io" "net/http" "net/url" + "strconv" "time" "github.com/cs3org/reva/v2/internal/http/services/ocmd" "github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/ocm/payload" "github.com/cs3org/reva/v2/pkg/rhttp" "github.com/pkg/errors" ) @@ -278,3 +281,68 @@ func (c *OCMClient) Discovery(ctx context.Context, endpoint string) (*Capabiliti 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)) +} diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/payload/payload.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/payload/payload.go new file mode 100644 index 00000000000..7c7ab80880b --- /dev/null +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/payload/payload.go @@ -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"` +} diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/json/json.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/json/json.go index 007022cd51b..013a126596d 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/json/json.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/json/json.go @@ -379,6 +379,12 @@ func receivedShareEqual(ref *ocm.ShareReference, s *ocm.ReceivedShare) bool { 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 } @@ -589,6 +595,23 @@ func (m *mgr) GetReceivedShare(ctx context.Context, user *userpb.User, ref *ocm. 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) { rs, err := m.GetReceivedShare(ctx, user, &ocm.ShareReference{Spec: &ocm.ShareReference_Id{Id: share.Id}}) if err != nil { diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud/nextcloud.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud/nextcloud.go index 7b89faf06fe..a51a3225b4a 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud/nextcloud.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/repository/nextcloud/nextcloud.go @@ -28,6 +28,7 @@ import ( "strings" 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" 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 } +// 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. func (sm *Manager) UpdateReceivedShare(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) { type paramsObj struct { diff --git a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/share.go b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/share.go index 665356ed3b1..6ad1601831d 100644 --- a/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/share.go +++ b/vendor/github.com/cs3org/reva/v2/pkg/ocm/share/share.go @@ -55,6 +55,9 @@ type Repository interface { // 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) + // 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(ctx context.Context, user *userpb.User, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) } diff --git a/vendor/modules.txt b/vendor/modules.txt index dfb4d5d51b6..5dc582a8d6d 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -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/tx/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 github.com/cs3org/reva/v2/cmd/revad/internal/grace 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/registry 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/authorizer/json github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/loader