github.com/cs3org/reva/v2@v2.27.7/internal/http/services/ocmd/notifications.go (about)

     1  // Copyright 2018-2023 CERN
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  //
    15  // In applying this license, CERN does not waive the privileges and immunities
    16  // granted to it by virtue of its status as an Intergovernmental Organization
    17  // or submit itself to any jurisdiction.
    18  
    19  package ocmd
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"fmt"
    25  	"mime"
    26  	"net/http"
    27  
    28  	gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
    29  	ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1"
    30  	rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
    31  	typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
    32  	"github.com/cs3org/reva/v2/pkg/appctx"
    33  	"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
    34  	"github.com/cs3org/reva/v2/pkg/utils"
    35  	"github.com/go-chi/render"
    36  )
    37  
    38  const (
    39  	SHARE_UNSHARED          = "SHARE_UNSHARED"
    40  	SHARE_CHANGE_PERMISSION = "SHARE_CHANGE_PERMISSION"
    41  )
    42  
    43  // var validate = validator.New()
    44  
    45  type notifHandler struct {
    46  	gatewaySelector *pool.Selector[gateway.GatewayAPIClient]
    47  }
    48  
    49  func (h *notifHandler) init(c *config) error {
    50  	gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc)
    51  	if err != nil {
    52  		return err
    53  	}
    54  	h.gatewaySelector = gatewaySelector
    55  
    56  	return nil
    57  }
    58  
    59  // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
    60  type notificationRequest struct {
    61  	NotificationType string `json:"notificationType" validate:"required"`
    62  	ResourceType     string `json:"resourceType" validate:"required"`
    63  	// 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.
    64  	ProviderId string `json:"providerId" validate:"required"`
    65  	// Optional additional parameters, depending on the notification and the resource type.
    66  	Notification *notification `json:"notification,omitempty"`
    67  }
    68  
    69  type notification struct {
    70  	Owner        string    `json:"owner,omitempty"`
    71  	Grantee      string    `json:"grantee,omitempty"`
    72  	SharedSecret string    `json:"sharedSecret,omitempty"`
    73  	Expiration   uint64    `json:"expiration,omitempty"`
    74  	Protocols    Protocols `json:"protocol,omitempty" validate:"required"`
    75  }
    76  
    77  // ErrorMessageResponse is the response returned by the OCM API in case of an error.
    78  // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post
    79  type ErrorMessageResponse struct {
    80  	Message          string             `json:"message"`
    81  	ValidationErrors []*ValidationError `json:"validationErrors,omitempty"`
    82  }
    83  
    84  // ValidationError is the payload for the validationErrors field in the ErrorMessageResponse.
    85  type ValidationError struct {
    86  	Name    string `json:"name"`
    87  	Message string `json:"message"`
    88  }
    89  
    90  // Notifications dispatches any notifications received from remote OCM sites
    91  // according to the specifications at:
    92  // https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1notifications/post
    93  func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) {
    94  	ctx := r.Context()
    95  	log := appctx.GetLogger(ctx)
    96  	req, err := getNotification(r)
    97  	if err != nil {
    98  		renderErrorBadRequest(w, r, http.StatusBadRequest, err.Error())
    99  		return
   100  	}
   101  
   102  	// TODO(lopresti) this is all to be implemented. For now we just log what we got
   103  	log.Debug().Msgf("Received OCM notification: %+v", req)
   104  
   105  	var status *rpc.Status
   106  	if req.Notification.Grantee == "" {
   107  		renderErrorBadRequest(w, r, http.StatusBadRequest, "grantee is required")
   108  	}
   109  	switch req.NotificationType {
   110  	case SHARE_UNSHARED:
   111  		status, err = h.handleShareUnshared(ctx, req)
   112  		if err != nil {
   113  			log.Err(err).Any("notificationRequest", req).Msg("error getting gateway client")
   114  			renderErrorBadRequest(w, r, http.StatusInternalServerError, status.GetMessage())
   115  		}
   116  	case SHARE_CHANGE_PERMISSION:
   117  		status, err = h.handleShareChangePermission(ctx, req)
   118  		if err != nil {
   119  			log.Err(err).Any("notificationRequest", req).Msg("error getting gateway client")
   120  			renderErrorBadRequest(w, r, http.StatusInternalServerError, status.GetMessage())
   121  		}
   122  	default:
   123  		renderErrorBadRequest(w, r, http.StatusBadRequest, "NotificationType "+req.NotificationType+" is not supported")
   124  		return
   125  	}
   126  	// parse the response status
   127  	switch status.GetCode() {
   128  	case rpc.Code_CODE_OK:
   129  		w.WriteHeader(http.StatusCreated)
   130  		return
   131  	case rpc.Code_CODE_INVALID_ARGUMENT:
   132  		renderErrorBadRequest(w, r, http.StatusBadRequest, status.GetMessage())
   133  		return
   134  	case rpc.Code_CODE_UNAUTHENTICATED:
   135  		w.WriteHeader(http.StatusUnauthorized)
   136  		return
   137  	case rpc.Code_CODE_PERMISSION_DENIED:
   138  		w.WriteHeader(http.StatusForbidden)
   139  		return
   140  	default:
   141  		log.Error().Str("code", status.GetCode().String()).Str("message", status.GetMessage()).Str("NotificationType", req.NotificationType).Msg("error handling notification")
   142  		w.WriteHeader(http.StatusInternalServerError)
   143  	}
   144  }
   145  
   146  func (h *notifHandler) handleShareUnshared(ctx context.Context, req *notificationRequest) (*rpc.Status, error) {
   147  	gatewayClient, err := h.gatewaySelector.Next()
   148  	if err != nil {
   149  		return nil, fmt.Errorf("error getting gateway client: %w", err)
   150  	}
   151  
   152  	o := &typesv1beta1.Opaque{}
   153  	utils.AppendPlainToOpaque(o, "grantee", req.Notification.Grantee)
   154  
   155  	res, err := gatewayClient.DeleteOCMCoreShare(ctx, &ocmcore.DeleteOCMCoreShareRequest{
   156  		Id:     req.ProviderId,
   157  		Opaque: o,
   158  	})
   159  	if err != nil {
   160  		return nil, fmt.Errorf("error calling DeleteOCMCoreShare: %w", err)
   161  	}
   162  	return res.GetStatus(), nil
   163  }
   164  
   165  func (h *notifHandler) handleShareChangePermission(ctx context.Context, req *notificationRequest) (*rpc.Status, error) {
   166  	gatewayClient, err := h.gatewaySelector.Next()
   167  	if err != nil {
   168  		return nil, fmt.Errorf("error getting gateway client: %w", err)
   169  	}
   170  
   171  	if req.Notification == nil || req.Notification.Protocols == nil {
   172  		return nil, fmt.Errorf("error getting protocols from notification")
   173  	}
   174  
   175  	o := &typesv1beta1.Opaque{}
   176  	utils.AppendPlainToOpaque(o, "grantee", req.Notification.Grantee)
   177  
   178  	res, err := gatewayClient.UpdateOCMCoreShare(ctx, &ocmcore.UpdateOCMCoreShareRequest{
   179  		OcmShareId: req.ProviderId,
   180  		Protocols:  getProtocols(req.Notification.Protocols),
   181  		Opaque:     o,
   182  	})
   183  	if err != nil {
   184  		return nil, fmt.Errorf("error calling DeleteOCMCoreShare: %w", err)
   185  	}
   186  	return res.GetStatus(), nil
   187  }
   188  
   189  func getNotification(r *http.Request) (*notificationRequest, error) {
   190  	contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type"))
   191  	if err == nil && contentType == "application/json" {
   192  		n := &notificationRequest{}
   193  		err := json.NewDecoder(r.Body).Decode(&n)
   194  		if err != nil {
   195  			return nil, err
   196  		}
   197  		return n, nil
   198  	}
   199  	return nil, err
   200  }
   201  
   202  func renderJSON(w http.ResponseWriter, r *http.Request, statusCode int, resp any) {
   203  	render.Status(r, statusCode)
   204  	render.JSON(w, r, resp)
   205  }
   206  
   207  func renderErrorBadRequest(w http.ResponseWriter, r *http.Request, statusCode int, message string) {
   208  	resp := &ErrorMessageResponse{
   209  		Message: "BAD_REQUEST",
   210  		ValidationErrors: []*ValidationError{
   211  			{
   212  				Name:    "notification",
   213  				Message: message,
   214  			},
   215  		},
   216  	}
   217  	renderJSON(w, r, http.StatusBadRequest, resp)
   218  }