From b66e85abd4e852bf86cdbf83bbdee41ee78aac16 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Mon, 30 Jan 2023 13:42:02 +0100 Subject: [PATCH 01/44] rewritten new share endpoint according to new ocm specs --- internal/http/services/ocmd/ocm.go | 4 +- internal/http/services/ocmd/protocols.go | 134 +++++++++ internal/http/services/ocmd/protocols_test.go | 190 +++++++++++++ internal/http/services/ocmd/shares.go | 267 ++++++++---------- 4 files changed, 452 insertions(+), 143 deletions(-) create mode 100644 internal/http/services/ocmd/protocols.go create mode 100644 internal/http/services/ocmd/protocols_test.go diff --git a/internal/http/services/ocmd/ocm.go b/internal/http/services/ocmd/ocm.go index 2974320a0e4..829a9126299 100644 --- a/internal/http/services/ocmd/ocm.go +++ b/internal/http/services/ocmd/ocm.go @@ -85,7 +85,9 @@ func (s *svc) routerInit() error { invitesHandler := new(invitesHandler) configHandler.init(s.Conf) - sharesHandler.init(s.Conf) + if err := sharesHandler.init(s.Conf); err != nil { + return err + } notificationsHandler.init(s.Conf) if err := invitesHandler.init(s.Conf); err != nil { return err diff --git a/internal/http/services/ocmd/protocols.go b/internal/http/services/ocmd/protocols.go new file mode 100644 index 00000000000..5bf7a38aa1f --- /dev/null +++ b/internal/http/services/ocmd/protocols.go @@ -0,0 +1,134 @@ +package ocmd + +import ( + "encoding/json" + "fmt" + "reflect" + "strings" + + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" +) + +type Protocols []Protocol + +type Protocol interface { + ToOCMProtocol() *ocm.Protocol +} + +// protocols supported by the OCM API + +// WebDAV contains the parameters for the WebDAV protocol. +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"` +} + +func (w *WebDAV) ToOCMProtocol() *ocm.Protocol { + perms := &ocm.SharePermissions{ + Permissions: &providerv1beta1.ResourcePermissions{}, + } + for _, p := range w.Permissions { + switch p { + case "read": + perms.Permissions.GetPath = true + perms.Permissions.InitiateFileDownload = true + perms.Permissions.ListContainer = true + perms.Permissions.Stat = true + case "write": + perms.Permissions.InitiateFileUpload = true + case "share": + perms.Reshare = true + } + } + + return &ocm.Protocol{ + Term: &ocm.Protocol_WebdapOptions{ + WebdapOptions: &ocm.WebDAVProtocol{ + SharedSecret: w.SharedSecret, + Uri: w.URL, + Permissions: perms, + }, + }, + } +} + +// Webapp contains the parameters for the Webapp protocol. +type Webapp struct { + URITemplate string `json:"uriTemplate" validate:"required"` +} + +func (w *Webapp) ToOCMProtocol() *ocm.Protocol { + return &ocm.Protocol{ + Term: &ocm.Protocol_WebappOptions{ + WebappOptions: &ocm.WebappProtocol{ + UriTemplate: w.URITemplate, + }, + }, + } +} + +// Datatx contains the parameters for the Datatx protocol. +type Datatx struct { + SharedSecret string `json:"sharedSecret" validate:"required"` + SourceURI string `json:"srcUri" validate:"required"` + Size uint64 `json:"size" validate:"required"` +} + +func (w *Datatx) ToOCMProtocol() *ocm.Protocol { + return &ocm.Protocol{ + Term: &ocm.Protocol_DatatxOprions{ + DatatxOprions: &ocm.DatatxProtocol{ + SharedSecret: w.SharedSecret, + SourceUri: w.SourceURI, + Size: w.Size, + }, + }, + } +} + +var protocolImpl = map[string]reflect.Type{ + "webdav": reflect.TypeOf(WebDAV{}), + "webapp": reflect.TypeOf(Webapp{}), + "datatx": reflect.TypeOf(Datatx{}), +} + +func (p *Protocols) UnmarshalJSON(data []byte) error { + var prot map[string]json.RawMessage + if err := json.Unmarshal(data, &prot); err != nil { + return err + } + + *p = []Protocol{} + + for name, d := range prot { + var res Protocol + ctype, ok := protocolImpl[name] + if !ok { + return fmt.Errorf("protocol %s not recognised", name) + } + + res = reflect.New(ctype).Interface().(Protocol) + if err := json.Unmarshal(d, &res); err != nil { + return err + } + + *p = append(*p, res) + } + return nil +} + +func (p Protocols) MarshalJSON() ([]byte, error) { + d := make(map[string]Protocol) + for _, prot := range p { + d[getProtocolName(prot)] = prot + } + return json.Marshal(d) +} + +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 new file mode 100644 index 00000000000..f397bb9fd48 --- /dev/null +++ b/internal/http/services/ocmd/protocols_test.go @@ -0,0 +1,190 @@ +package ocmd + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/gdexlab/go-render/render" +) + +func TestUnmarshalProtocol(t *testing.T) { + tests := []struct { + raw string + expected Protocols + err string + }{ + { + raw: "{}", + expected: []Protocol{}, + }, + { + raw: `{"webdav":{"sharedSecret":"secret","permissions":["read","write"],"url":"http://example.org"}}`, + expected: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read", "write"}, + URL: "http://example.org", + }, + }, + }, + { + raw: `{"webapp":{"uriTemplate":"http://example.org/{test}"}}`, + expected: []Protocol{ + &Webapp{ + URITemplate: "http://example.org/{test}", + }, + }, + }, + { + raw: `{"datatx":{"sharedSecret":"secret","srcUri":"http://example.org","size":10}}`, + expected: []Protocol{ + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org", + Size: 10, + }, + }, + }, + { + raw: `{"webdav":{"sharedSecret":"secret","permissions":["read","write"],"url":"http://example.org"},"webapp":{"uriTemplate":"http://example.org/{test}"},"datatx":{"sharedSecret":"secret","srcUri":"http://example.org","size":10}}`, + expected: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read", "write"}, + URL: "http://example.org", + }, + &Webapp{ + URITemplate: "http://example.org/{test}", + }, + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org", + Size: 10, + }, + }, + }, + { + raw: `{"not_existing":{}}`, + err: "protocol not_existing not recognised", + }, + } + + for _, tt := range tests { + var got Protocols + err := json.Unmarshal([]byte(tt.raw), &got) + if err != nil && err.Error() != tt.err { + t.Fatalf("not expected error. got=%+v expected=%+v", err, tt.err) + } + + if tt.err == "" { + if !reflect.DeepEqual(got, tt.expected) { + t.Fatalf("result does not match with expected. got=%+v expected=%+v", render.AsCode(got), render.AsCode(tt.expected)) + } + } + } +} + +func TestMarshalProtocol(t *testing.T) { + tests := []struct { + in Protocols + expected map[string]map[string]any + }{ + { + in: []Protocol{}, + expected: map[string]map[string]any{}, + }, + { + in: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read"}, + URL: "http://example.org", + }, + }, + expected: map[string]map[string]any{ + "webdav": { + "sharedSecret": "secret", + "permissions": []any{"read"}, + "url": "http://example.org", + }, + }, + }, + { + in: []Protocol{ + &Webapp{ + URITemplate: "http://example.org", + }, + }, + expected: map[string]map[string]any{ + "webapp": { + "uriTemplate": "http://example.org", + }, + }, + }, + { + in: []Protocol{ + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org/source", + Size: 10, + }, + }, + expected: map[string]map[string]any{ + "datatx": { + "sharedSecret": "secret", + "srcUri": "http://example.org/source", + "size": float64(10), + }, + }, + }, + { + in: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read"}, + URL: "http://example.org", + }, + &Webapp{ + URITemplate: "http://example.org", + }, + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org/source", + Size: 10, + }, + }, + expected: map[string]map[string]any{ + "webdav": { + "sharedSecret": "secret", + "permissions": []any{"read"}, + "url": "http://example.org", + }, + "webapp": { + "uriTemplate": "http://example.org", + }, + "datatx": { + "sharedSecret": "secret", + "srcUri": "http://example.org/source", + "size": float64(10), + }, + }, + }, + } + + for _, tt := range tests { + d, err := json.Marshal(tt.in) + if err != nil { + t.Fatal("not expected error", err) + } + + var got map[string]map[string]any + if err := json.Unmarshal(d, &got); err != nil { + t.Fatal("not expected error", err) + } + + if !reflect.DeepEqual(tt.expected, got) { + t.Fatalf("result does not match with expected. got=%+v expected=%+v", render.AsCode(got), render.AsCode(tt.expected)) + } + } +} diff --git a/internal/http/services/ocmd/shares.go b/internal/http/services/ocmd/shares.go index a4dbd9699b0..153040501e1 100644 --- a/internal/http/services/ocmd/shares.go +++ b/internal/http/services/ocmd/shares.go @@ -22,89 +22,68 @@ import ( "encoding/json" "errors" "fmt" - "io" - "math" "mime" "net/http" - "reflect" "strings" - "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/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" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/reqres" - "github.com/cs3org/reva/pkg/appctx" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/pkg/utils" + "github.com/go-playground/validator/v10" ) +var validate = validator.New() + type sharesHandler struct { - gatewayAddr string + gatewayClient gateway.GatewayAPIClient +} + +func (h *sharesHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySvc)) + if err != nil { + return err + } + return nil } -func (h *sharesHandler) init(c *config) { - h.gatewayAddr = c.GatewaySvc +type createShareRequest struct { + ShareWith string `json:"shareWith" validate:"required"` // identifier of the recipient of the share + Name string `json:"name" validate:"required"` // name of the resource + Description string `json:"description"` // (optional) description of the resource + ResourceID string `json:"resourceId" validate:"required"` // unique identifier of the resource at provider side + Owner string `json:"owner" validate:"required"` // unique identifier of the owner at provider side + Sender string `json:"sender" validate:"required"` // unique indentifier of the user who wants to share the resource at provider side + OwnerDisplayName string `json:"ownerDisplayName"` // display name of the owner of the resource + SenderDisplayName string `json:"senderDisplayName"` // dispay name of the user who wants to share the resource + ShareType string `json:"shareType" validate:"required,oneof=user group"` // recipient share type (user or group) + ResourceType string `json:"resourceType" validate:"required,oneof=file folder"` + Protocols Protocols `json:"protocols" validate:"required"` } // CreateShare sends all the informations to the consumer needed to start // synchronization between the two services. func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - log := appctx.GetLogger(ctx) - contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - var shareWith, meshProvider, resource, providerID, owner string - var protocol map[string]interface{} - if err == nil && contentType == "application/json" { - defer r.Body.Close() - reqBody, err := io.ReadAll(r.Body) - if err == nil { - reqMap := make(map[string]interface{}) - err = json.Unmarshal(reqBody, &reqMap) - if err == nil { - meshProvider = reqMap["meshProvider"].(string) // FIXME: get this from sharedBy string? - shareWith, protocol = reqMap["shareWith"].(string), reqMap["protocol"].(map[string]interface{}) - resource, owner = reqMap["name"].(string), reqMap["owner"].(string) - // Note that if an OCM request were to go directly from a Nextcloud server - // to a Reva server, it will (incorrectly) sends an integer provider_id instead a string one. - // This doesn't happen when using the sciencemesh-nextcloud app, but in order to make the OCM - // test suite pass, this code works around that: - if reflect.ValueOf(reqMap["providerId"]).Kind() == reflect.Float64 { - providerID = fmt.Sprintf("%d", int(math.Round(reqMap["providerId"].(float64)))) - } else { - providerID = reqMap["providerId"].(string) - } - } else { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "could not parse json request body", nil) - } - } - } else { - var protocolJSON string - shareWith, protocolJSON, meshProvider = r.FormValue("shareWith"), r.FormValue("protocol"), r.FormValue("meshProvider") - resource, providerID, owner = r.FormValue("name"), r.FormValue("providerId"), r.FormValue("owner") - err = json.Unmarshal([]byte(protocolJSON), &protocol) - if err != nil { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "invalid protocol parameters", nil) - } - } - if resource == "" || providerID == "" || owner == "" { - msg := fmt.Sprintf("missing details about resource to be shared (resource='%s', providerID='%s', owner='%s", resource, providerID, owner) - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, msg, nil) - return - } - if shareWith == "" || protocol["name"] == "" || meshProvider == "" { - msg := fmt.Sprintf("missing request parameters (shareWith='%s', protocol.name='%s', meshProvider='%s'", shareWith, protocol["name"], meshProvider) - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, msg, nil) + req, err := getCreateShareRequest(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - gatewayClient, err := pool.GetGatewayServiceClient(pool.Endpoint(h.gatewayAddr)) + _, meshProvider, err := getIdAndMeshProvider(req.Sender) if err != nil { - reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting storage grpc client", err) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } @@ -122,7 +101,7 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { }, } - providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{ + providerAllowedResp, err := h.gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{ Provider: &providerInfo, }) if err != nil { @@ -134,9 +113,14 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { return } - var shareWithParts = strings.Split(shareWith, "@") - userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ - UserId: &userpb.UserId{OpaqueId: shareWithParts[0]}, SkipFetchingUserGroups: true, + shareWith, _, err := getIdAndMeshProvider(req.ShareWith) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) + return + } + + userRes, err := h.gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ + UserId: &userpb.UserId{OpaqueId: shareWith}, SkipFetchingUserGroups: true, }) if err != nil { reqres.WriteError(w, r, reqres.APIErrorServerError, "error searching recipient", err) @@ -147,105 +131,104 @@ func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { return } - var permissions conversions.Permissions - var token string - options, ok := protocol["options"].(map[string]interface{}) - if !ok { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "protocol: webdav token not provided", nil) + owner, err := getUserIDFromOCMUser(req.Owner) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - token, ok = options["sharedSecret"].(string) - if !ok { - token, ok = options["token"].(string) - if !ok { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "protocol: webdav token not provided", nil) - return - } - } - var role *conversions.Role - pval, ok := options["permissions"].(int) - if !ok { - role = conversions.NewViewerRole() - } else { - permissions, err = conversions.NewPermissions(pval) - if err != nil { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) - return - } - role = conversions.RoleFromOCSPermissions(permissions) - } - - val, err := json.Marshal(role.CS3ResourcePermissions()) + sender, err := getUserIDFromOCMUser(req.Sender) if err != nil { - reqres.WriteError(w, r, reqres.APIErrorServerError, "could not encode role", nil) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - ownerParts := strings.Split(owner, "@") - if len(ownerParts) != 2 { - reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "owner should be opaqueId@webDAVHost", nil) - } - ownerID := &userpb.UserId{ - OpaqueId: ownerParts[0], - Idp: ownerParts[1], - Type: userpb.UserType_USER_TYPE_PRIMARY, - } - createShareReq := &ocmcore.CreateOCMCoreShareRequest{ - Name: resource, - ProviderId: providerID, - Owner: ownerID, - ShareWith: userRes.User.GetId(), - Protocol: &ocmcore.Protocol{ - Name: protocol["name"].(string), - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "json", - Value: val, - }, - "token": { - Decoder: "plain", - Value: []byte(token), - }, - }, - }, - }, - } - createShareResponse, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq) + createShareResp, err := h.gatewayClient.CreateOCMCoreShare(ctx, &ocmcore.CreateOCMCoreShareRequest{ + Description: req.Description, + Name: req.Name, + ResourceId: req.ResourceID, + Owner: owner, + Sender: sender, + ShareWith: userRes.User.Id, + ResourceType: getResourceTypeFromOCMRequest(req.ResourceType), + ShareType: getOCMShareType(req.ShareType), + Protocols: getProtocols(req.Protocols), + }) if err != nil { - reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc create ocm core share request", err) - return - } - if createShareResponse.Status.Code != rpc.Code_CODE_OK { - if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - reqres.WriteError(w, r, reqres.APIErrorNotFound, "not found", nil) - return - } - reqres.WriteError(w, r, reqres.APIErrorServerError, "grpc create ocm core share request failed", errors.New(createShareResponse.Status.Message)) + reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", err) return } - timeCreated := createShareResponse.Created - jsonOut, err := json.Marshal( - map[string]string{ - "id": createShareResponse.Id, - "createdAt": time.Unix(int64(timeCreated.Seconds), int64(timeCreated.Nanos)).String(), - }, - ) - if err != nil { - reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling share data", err) + if userRes.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 } w.WriteHeader(http.StatusCreated) - w.Header().Set("Content-Type", "application/json") +} - _, err = w.Write(jsonOut) +func getUserIDFromOCMUser(user string) (*userpb.UserId, error) { + id, idp, err := getIdAndMeshProvider(user) if err != nil { - reqres.WriteError(w, r, reqres.APIErrorServerError, "error writing shares data", err) - return + return nil, err + } + return &userpb.UserId{ + OpaqueId: id, + Idp: idp, + // the remote user is a federated account for the local reva + Type: userpb.UserType_USER_TYPE_FEDERATED, + }, nil +} + +func getIdAndMeshProvider(user string) (string, string, error) { + // the user is in the form of dimitri@apiwise.nl + split := strings.Split(user, "@") + if len(split) < 2 { + return "", "", errors.New("not in the form @") + } + return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil +} + +func getCreateShareRequest(r *http.Request) (*createShareRequest, error) { + var req createShareRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + return nil, errors.New("body request not recognised") + } + // validate the request + if err := validate.Struct(req); err != nil { + return nil, err + } + return &req, nil +} + +func getResourceTypeFromOCMRequest(t string) providerpb.ResourceType { + switch t { + case "file": + return providerpb.ResourceType_RESOURCE_TYPE_FILE + case "folder": + return providerpb.ResourceType_RESOURCE_TYPE_CONTAINER + default: + return providerpb.ResourceType_RESOURCE_TYPE_INVALID } +} - log.Info().Msg("Share created.") +func getOCMShareType(t string) ocm.ShareType { + if t == "user" { + return ocm.ShareType_SHARE_TYPE_USER + } + return ocm.ShareType_SHARE_TYPE_GROUP +} + +func getProtocols(p Protocols) []*ocm.Protocol { + var prot []*ocm.Protocol + for _, data := range p { + prot = append(prot, data.ToOCMProtocol()) + } + return prot } From 0802220815ac21a386af80a8aa332de696ccd463 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Mon, 30 Jan 2023 13:44:01 +0100 Subject: [PATCH 02/44] add go validator dependency --- go.mod | 5 +++++ go.sum | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/go.mod b/go.mod index ae5ee497ef0..1a5064e0065 100644 --- a/go.mod +++ b/go.mod @@ -94,6 +94,9 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/errors v0.20.2 // indirect github.com/go-openapi/strfmt v0.21.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/go-stack/stack v1.8.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect @@ -109,6 +112,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.11 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/leodido/go-urn v1.2.1 // indirect github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect @@ -163,6 +167,7 @@ require ( go 1.19 replace ( + github.com/cs3org/go-cs3apis => /home/gianmaria/Documents/CERN/go-cs3apis github.com/eventials/go-tus => github.com/andrewmostello/go-tus v0.0.0-20200314041820-904a9904af9a github.com/oleiade/reflections => github.com/oleiade/reflections v1.0.1 ) diff --git a/go.sum b/go.sum index ac5ce943f2e..32d4914ac5a 100644 --- a/go.sum +++ b/go.sum @@ -397,8 +397,14 @@ github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/e github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= +github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU= +github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s= github.com/go-resty/resty/v2 v2.1.1-0.20191201195748-d7b97669fe48/go.mod h1:dZGr0i9PLlaaTD4H/hoZIDjQ+r6xq8mgbRzHZf7f2J8= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -687,6 +693,8 @@ github.com/labbsr0x/goh v1.0.1/go.mod h1:8K2UhVoaWXcCU7Lxoa2omWnC8gyW8px7/lmO61c github.com/labstack/echo/v4 v4.1.11/go.mod h1:i541M3Fj6f76NZtHSj7TXnyM8n2gaodfvfxNnFqi74g= github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= +github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/linode/linodego v0.25.3/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE= From f46b0c4f3657a47c17ee587c1ec2c20f3f0093dd Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Mon, 30 Jan 2023 15:44:12 +0100 Subject: [PATCH 03/44] add new share to ocm client --- pkg/ocm/client/client.go | 73 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/pkg/ocm/client/client.go b/pkg/ocm/client/client.go index fa6214a8350..2d861d12cbd 100644 --- a/pkg/ocm/client/client.go +++ b/pkg/ocm/client/client.go @@ -27,6 +27,7 @@ import ( "net/url" "time" + "github.com/cs3org/reva/internal/http/services/ocmd" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rhttp" "github.com/pkg/errors" @@ -48,6 +49,10 @@ var ErrUserAlreadyAccepted = errors.New("user already accepted an invitation tok // endpoint when the request is done using a not existing token. var ErrTokenNotFound = errors.New("token not found") +// ErrInvalidParameters is the error returned by the shares endpoint +// when the request does not contain required properties. +var ErrInvalidParameters = errors.New("invalid parameters") + // OCMClient is the client for an OCM provider. type OCMClient struct { client *http.Client @@ -147,3 +152,71 @@ func (c *OCMClient) parseInviteAcceptedResponse(r *http.Response) (*User, error) } return nil, errtypes.InternalError(string(body)) } + +// NewShareRequest contains the parameters for creating a new OCM share. +type NewShareRequest struct { + ShareWith string `json:"shareWith"` + Name string `json:"name"` + Description string `json:"description"` + ResourceID string `json:"resourceId"` + Owner string `json:"owner"` + Sender string `json:"sender"` + OwnerDisplayName string `json:"ownerDisplayName"` + SenderDisplayName string `json:"senderDisplayName"` + ShareType string `json:"shareType"` + ResourceType string `json:"resourceType"` + Protocols ocmd.Protocols `json:"protocols"` +} + +func (r *NewShareRequest) toJSON() (io.Reader, error) { + var b bytes.Buffer + if err := json.NewEncoder(&b).Encode(r); err != nil { + return nil, err + } + return &b, nil +} + +// NewShare creates a new share. +// https://github.com/cs3org/OCM-API/blob/223285aa4d828ed85c361c7382efd08c42b5e719/spec.yaml +func (c *OCMClient) NewShare(ctx context.Context, endpoint string, r *NewShareRequest) error { + url, err := url.JoinPath(endpoint, "shares") + 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() + + return c.parseNewShareError(resp) +} + +func (c *OCMClient) parseNewShareError(r *http.Response) error { + switch r.StatusCode { + case http.StatusOK, http.StatusCreated: + return nil + case http.StatusBadRequest: + return ErrInvalidParameters + case http.StatusUnauthorized, http.StatusForbidden: + return ErrServiceNotTrusted + } + + body, err := io.ReadAll(r.Body) + if err != nil { + return errors.Wrap(err, "error decoding response body") + } + return errtypes.InternalError(string(body)) +} From 8eb474be853673deb9b4d6bbb1bcb14f91435f32 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Mon, 30 Jan 2023 15:44:25 +0100 Subject: [PATCH 04/44] defined ocm repository --- pkg/ocm/share/share.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pkg/ocm/share/share.go b/pkg/ocm/share/share.go index 1ced096455a..4ab05bb639c 100644 --- a/pkg/ocm/share/share.go +++ b/pkg/ocm/share/share.go @@ -57,6 +57,34 @@ type Manager interface { UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) } +// Repository is the interface that manipulates the OCM shares repositories. +type Repository interface { + // StoreShare stores a share. + StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) + + // GetShare gets the information for a share by the given ref. + GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) + + // DeleteShare deletes the share pointed by ref. + DeleteShare(ctx context.Context, ref *ocm.ShareReference) error + + // UpdateShare updates the mode of the given share. + UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) + + // ListShares returns the shares created by the user. If md is provided is not nil, + // it returns only shares attached to the given resource. + ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) + + // ListReceivedShares returns the list of shares the user has access. + ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) + + // GetReceivedShare returns the information for a received share the user has access. + GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) + + // UpdateReceivedShare updates the received share with share state. + UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) +} + // ResourceIDFilter is an abstraction for creating filter by resource id. func ResourceIDFilter(id *provider.ResourceId) *ocm.ListOCMSharesRequest_Filter { return &ocm.ListOCMSharesRequest_Filter{ From ddfe37eea368ece37cc03cf2ee4b7599e9e5ff8b Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Wed, 1 Feb 2023 14:33:52 +0100 Subject: [PATCH 05/44] rewrote ocm core --- internal/grpc/services/ocmcore/ocmcore.go | 109 +--- pkg/ocm/share/repository/json/json.go | 538 ++++++++++++++++++ pkg/ocm/share/repository/loader/loader.go | 26 + pkg/ocm/share/repository/registry/registry.go | 34 ++ pkg/ocm/share/share.go | 51 +- 5 files changed, 636 insertions(+), 122 deletions(-) create mode 100644 pkg/ocm/share/repository/json/json.go create mode 100644 pkg/ocm/share/repository/loader/loader.go create mode 100644 pkg/ocm/share/repository/registry/registry.go diff --git a/internal/grpc/services/ocmcore/ocmcore.go b/internal/grpc/services/ocmcore/ocmcore.go index adc960cf199..c7521b6c51c 100644 --- a/internal/grpc/services/ocmcore/ocmcore.go +++ b/internal/grpc/services/ocmcore/ocmcore.go @@ -20,16 +20,14 @@ package ocmcore import ( "context" - "encoding/json" "fmt" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/ocm/share" - "github.com/cs3org/reva/pkg/ocm/share/manager/registry" + "github.com/cs3org/reva/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" "github.com/mitchellh/mapstructure" @@ -48,7 +46,7 @@ type config struct { type service struct { conf *config - sm share.Manager + repo share.Repository } func (c *config) init() { @@ -61,7 +59,7 @@ func (s *service) Register(ss *grpc.Server) { ocmcore.RegisterOcmCoreAPIServer(ss, s) } -func getShareManager(c *config) (share.Manager, error) { +func getShareRepository(c *config) (share.Repository, error) { if f, ok := registry.NewFuncs[c.Driver]; ok { return f(c.Drivers[c.Driver]) } @@ -85,14 +83,14 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { } c.init() - sm, err := getShareManager(c) + repo, err := getShareRepository(c) if err != nil { return nil, err } service := &service{ conf: c, - sm: sm, + repo: repo, } return service, nil @@ -108,91 +106,36 @@ func (s *service) UnprotectedEndpoints() []string { // CreateOCMCoreShare is called when an OCM request comes into this reva instance from. func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) { - resource := &provider.ResourceId{ - StorageId: "remote", - OpaqueId: req.Name, + if req.ShareType != ocm.ShareType_SHARE_TYPE_USER { + return nil, errtypes.NotSupported("share type not supported") } - var resourcePermissions *provider.ResourcePermissions - permOpaque, ok := req.Protocol.Opaque.Map["permissions"] - if !ok { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, errtypes.BadRequest("resource permissions not set"), ""), - }, nil - } - switch permOpaque.Decoder { - case "json": - err := json.Unmarshal(permOpaque.Value, &resourcePermissions) - if err != nil { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, err, "error decoding resource permissions"), - }, nil - } - default: - err := errtypes.NotSupported("opaque entry decoder not recognized") - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, err, "invalid opaque entry decoder"), - }, nil - } - - var token string - tokenOpaque, ok := req.Protocol.Opaque.Map["token"] - if !ok { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, errtypes.PermissionDenied("token not set"), ""), - }, nil - } - switch tokenOpaque.Decoder { - case "plain": - token = string(tokenOpaque.Value) - default: - err := errtypes.NotSupported("opaque entry decoder not recognized: " + tokenOpaque.Decoder) - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, err, "invalid opaque entry decoder"), - }, nil - } - - grant := &ocm.ShareGrant{ - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - // For now, we only support user shares. - // TODO (ishank011): To be updated once this is decided. - Id: &provider.Grantee_UserId{UserId: req.ShareWith}, - // passing this in grant.Grantee.Opaque because ShareGrant itself doesn't have a root opaque. - Opaque: &typespb.Opaque{ - Map: map[string]*typespb.OpaqueEntry{ - "remoteShareId": { - Decoder: "plain", - Value: []byte(req.ProviderId), - }, - }, - }, + share, err := s.repo.StoreReceivedShare(ctx, &ocm.ReceivedShare{ + ResourceId: &providerpb.ResourceId{ + OpaqueId: req.ResourceId, }, - Permissions: &ocm.SharePermissions{ - Permissions: resourcePermissions, + Name: req.Name, + Grantee: &providerpb.Grantee{ + Type: providerpb.GranteeType_GRANTEE_TYPE_USER, + Id: &providerpb.Grantee_UserId{ + UserId: req.ShareWith, + }, }, - } - - var shareType ocm.Share_ShareType - switch req.Protocol.Name { - case "datatx": - shareType = ocm.Share_SHARE_TYPE_TRANSFER - default: - shareType = ocm.Share_SHARE_TYPE_REGULAR - } - - share, err := s.sm.Share(ctx, resource, grant, req.Name, nil, "", req.Owner, token, shareType) - + Owner: req.Owner, + Creator: req.Sender, + Protocols: req.Protocols, + State: ocm.ShareState_SHARE_STATE_PENDING, + }) if err != nil { + // TODO: identify errors return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, err, "error creating ocm core share"), + Status: status.NewInternal(ctx, err, err.Error()), }, nil } - res := &ocmcore.CreateOCMCoreShareResponse{ + return &ocmcore.CreateOCMCoreShareResponse{ Status: status.NewOK(ctx), Id: share.Id.OpaqueId, Created: share.Ctime, - } - return res, nil + }, nil } diff --git a/pkg/ocm/share/repository/json/json.go b/pkg/ocm/share/repository/json/json.go new file mode 100644 index 00000000000..57c15cc95d1 --- /dev/null +++ b/pkg/ocm/share/repository/json/json.go @@ -0,0 +1,538 @@ +// Copyright 2018-2023 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 json + +import ( + "context" + "encoding/json" + "os" + "sync" + "time" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/ocm/share" + "github.com/cs3org/reva/pkg/ocm/share/repository/registry" + "github.com/cs3org/reva/pkg/utils" + "github.com/google/uuid" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "google.golang.org/genproto/protobuf/field_mask" +) + +func init() { + registry.Register("json", New) +} + +// New returns a new authorizer object. +func New(m map[string]interface{}) (share.Repository, error) { + c, err := parseConfig(m) + if err != nil { + err = errors.Wrap(err, "error creating a new manager") + return nil, err + } + c.init() + + // load or create file + model, err := loadOrCreate(c.File) + if err != nil { + err = errors.Wrap(err, "error loading the file containing the shares") + return nil, err + } + + mgr := &mgr{ + c: c, + model: model, + } + + return mgr, nil +} + +func loadOrCreate(file string) (*shareModel, error) { + _, err := os.Stat(file) + if os.IsNotExist(err) { + if err := os.WriteFile(file, []byte("{}"), 0700); err != nil { + return nil, errors.Wrap(err, "error creating the file: "+file) + } + } + + f, err := os.OpenFile(file, os.O_RDONLY, 0644) + if err != nil { + err = errors.Wrap(err, "error opening the file: "+file) + return nil, err + } + defer f.Close() + + var m shareModel + if err := json.NewDecoder(f).Decode(&m); err != nil { + return nil, errors.Wrap(err, "error decoding data to json") + } + + if m.Shares == nil { + m.Shares = map[string]*ocm.Share{} + } + if m.ReceivedShares == nil { + m.ReceivedShares = map[string]*ocm.ReceivedShare{} + } + m.file = file + + return &m, nil +} + +type shareModel struct { + file string `json:"-"` + Shares map[string]*ocm.Share `json:"shares"` // share_id -> share + ReceivedShares map[string]*ocm.ReceivedShare `json:"received_shares"` // share_id -> share +} + +type config struct { + File string `mapstructure:"file"` + InsecureConnections bool `mapstructure:"insecure_connections"` +} + +func (c *config) init() { + if c.File == "" { + c.File = "/var/tmp/reva/ocm-shares.json" + } +} + +type mgr struct { + c *config + sync.Mutex // concurrent access to the file + model *shareModel +} + +func (m *shareModel) save() error { + f, err := os.OpenFile(m.file, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return errors.Wrap(err, "error opening file "+m.file) + } + defer f.Close() + + if err := json.NewEncoder(f).Encode(m); err != nil { + return errors.Wrap(err, "error encoding to json") + } + + return nil +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + return c, nil +} + +func genID() string { + return uuid.New().String() +} + +func (m *mgr) StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) { + m.Lock() + defer m.Unlock() + + now := time.Now().UnixNano() + ts := &typespb.Timestamp{ + Seconds: uint64(now / 1000000000), + Nanos: uint32(now % 1000000000), + } + + share.Id = &ocm.ShareId{ + OpaqueId: genID(), + } + share.Mtime = ts + share.Ctime = ts + + m.model.Shares[share.Id.OpaqueId] = cloneShare(share) + + if err := m.model.save(); err != nil { + return nil, errors.Wrap(err, "error saving share") + } + + return share, nil +} + +func cloneShare(s *ocm.Share) *ocm.Share { + d, err := utils.MarshalProtoV1ToJSON(s) + if err != nil { + panic(err) + } + var cloned ocm.Share + if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil { + panic(err) + } + return &cloned +} + +func cloneReceivedShare(s *ocm.ReceivedShare) *ocm.ReceivedShare { + d, err := utils.MarshalProtoV1ToJSON(s) + if err != nil { + panic(err) + } + var cloned ocm.ReceivedShare + if err := utils.UnmarshalJSONToProtoV1(d, &cloned); err != nil { + panic(err) + } + return &cloned +} + +func (m *mgr) GetShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) (*ocm.Share, error) { + var ( + s *ocm.Share + err error + ) + + switch { + case ref.GetId() != nil: + s, err = m.getByID(ctx, ref.GetId()) + case ref.GetKey() != nil: + s, err = m.getByKey(ctx, ref.GetKey()) + default: + err = errtypes.NotFound(ref.String()) + } + + if err != nil { + return nil, err + } + + // check if we are the owner + if utils.UserEqual(user, s.Owner) || utils.UserEqual(user, s.Creator) { + return s, nil + } + + return nil, errtypes.NotFound(ref.String()) +} + +// // Called from both grpc CreateOCMShare for outgoing +// // and http /ocm/shares for incoming +// // pi is provider info +// // pm is permissions. +// func (m *mgr) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string, +// pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) { +// id := genID() +// now := time.Now().UnixNano() +// ts := &typespb.Timestamp{ +// Seconds: uint64(now / 1000000000), +// Nanos: uint32(now % 1000000000), +// } + +// // Since both OCMCore and OCMShareProvider use the same package, we distinguish +// // between calls received from them on the basis of whether they provide info +// // about the remote provider on which the share is to be created. +// // If this info is provided, this call is on the owner's mesh provider and so +// // we call the CreateOCMCoreShare method on the remote provider as well, +// // else this is received from another provider and we only create a local share. +// var isOwnersMeshProvider bool +// if pi != nil { +// isOwnersMeshProvider = true +// } + +// var userID *userpb.UserId +// if !isOwnersMeshProvider { +// // Since this call is on the remote provider, the owner of the resource is expected to be specified. +// if owner == nil { +// return nil, errors.New("json: owner of resource not provided") +// } +// userID = owner +// g.Grantee.Opaque = &typespb.Opaque{ +// Map: map[string]*typespb.OpaqueEntry{ +// "token": { +// Decoder: "plain", +// Value: []byte(token), +// }, +// }, +// } +// } else { +// userID = ctxpkg.ContextMustGetUser(ctx).GetId() +// } + +// // do not allow share to myself if share is for a user +// if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(g.Grantee.GetUserId(), userID) { +// return nil, errors.New("json: user and grantee are the same") +// } + +// // check if share already exists. +// key := &ocm.ShareKey{ +// Owner: userID, +// ResourceId: md, +// Grantee: g.Grantee, +// } +// _, err := m.getByKey(ctx, key) + +// // share already exists +// if isOwnersMeshProvider && err == nil { +// return nil, errtypes.AlreadyExists(key.String()) +// } + +// s := &ocm.Share{ +// Id: &ocm.ShareId{ +// OpaqueId: id, +// }, +// Name: name, +// ResourceId: md, +// Permissions: g.Permissions, +// Grantee: g.Grantee, +// Owner: userID, +// Creator: userID, +// Ctime: ts, +// Mtime: ts, +// ShareType: st, +// } + +// if isOwnersMeshProvider { +// protocol := map[string]interface{}{ +// "name": "webdav", +// "options": map[string]string{ +// "permissions": pm, +// "token": ctxpkg.ContextMustGetToken(ctx), +// }, +// } +// if st == ocm.Share_SHARE_TYPE_TRANSFER { +// protocol["name"] = "datatx" +// } + +// requestBodyMap := map[string]interface{}{ +// "shareWith": g.Grantee.GetUserId().OpaqueId, +// "name": name, +// "providerId": fmt.Sprintf("%s:%s", md.StorageId, md.OpaqueId), +// "owner": fmt.Sprintf("%s@%s", userID.OpaqueId, userID.Idp), +// "protocol": protocol, +// "meshProvider": userID.Idp, +// } +// err = sender.Send(ctx, requestBodyMap, pi) +// if err != nil { +// err = errors.Wrap(err, "error sending OCM POST") +// return nil, err +// } +// } + +// m.Lock() +// if err := m.model.ReadFile(); err != nil { +// err = errors.Wrap(err, "error reading model") +// return nil, err +// } + +// if isOwnersMeshProvider { +// encShare, err := utils.MarshalProtoV1ToJSON(s) +// if err != nil { +// return nil, err +// } +// m.model.Shares[s.Id.OpaqueId] = string(encShare) +// } else { +// encShare, err := utils.MarshalProtoV1ToJSON(&ocm.ReceivedShare{ +// Share: s, +// State: ocm.ShareState_SHARE_STATE_PENDING, +// }) +// if err != nil { +// return nil, err +// } +// m.model.ReceivedShares[s.Id.OpaqueId] = string(encShare) +// } + +// if err := m.model.Save(); err != nil { +// err = errors.Wrap(err, "error saving model") +// return nil, err +// } +// m.Unlock() + +// return s, nil +// } + +func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) { + m.Lock() + defer m.Unlock() + + if share, ok := m.model.Shares[id.OpaqueId]; ok { + return share, nil + } + return nil, errtypes.NotFound(id.String()) +} + +func (m *mgr) getByKey(ctx context.Context, key *ocm.ShareKey) (*ocm.Share, error) { + m.Lock() + defer m.Unlock() + + for _, share := range m.model.Shares { + if (utils.UserEqual(key.Owner, share.Owner) || utils.UserEqual(key.Owner, share.Creator)) && + utils.ResourceIDEqual(key.ResourceId, share.ResourceId) && utils.GranteeEqual(key.Grantee, share.Grantee) { + return share, nil + } + } + return nil, errtypes.NotFound(key.String()) +} + +func (m *mgr) DeleteShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) error { + m.Lock() + defer m.Unlock() + + for id, share := range m.model.Shares { + if sharesEqual(ref, share) { + if utils.UserEqual(user, share.Owner) || utils.UserEqual(user, share.Creator) { + delete(m.model.Shares, id) + if err := m.model.save(); err != nil { + return err + } + return nil + } + } + } + return errtypes.NotFound(ref.String()) +} + +func sharesEqual(ref *ocm.ShareReference, s *ocm.Share) bool { + if ref.GetId() != nil && s.Id != nil { + if ref.GetId().OpaqueId == s.Id.OpaqueId { + return true + } + } else if ref.GetKey() != nil { + if (utils.UserEqual(ref.GetKey().Owner, s.Owner) || utils.UserEqual(ref.GetKey().Owner, s.Creator)) && + utils.ResourceIDEqual(ref.GetKey().ResourceId, s.ResourceId) && utils.GranteeEqual(ref.GetKey().Grantee, s.Grantee) { + return true + } + } + return false +} + +func receivedShareEqual(ref *ocm.ShareReference, s *ocm.ReceivedShare) bool { + if ref.GetId() != nil && s.Id != nil { + if ref.GetId().OpaqueId == s.Id.OpaqueId { + return true + } + } else if ref.GetKey() != nil { + + if (utils.UserEqual(ref.GetKey().Owner, s.Owner) || utils.UserEqual(ref.GetKey().Owner, s.Creator)) && + utils.ResourceIDEqual(ref.GetKey().ResourceId, s.ResourceId) && utils.GranteeEqual(ref.GetKey().Grantee, s.Grantee) { + return true + } + } + return false +} + +func (m *mgr) UpdateShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) { + return nil, errtypes.NotSupported("not yet implemented") +} + +func (m *mgr) ListShares(ctx context.Context, user *userpb.UserId, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) { + var ss []*ocm.Share + + m.Lock() + defer m.Unlock() + + for _, share := range m.model.Shares { + if utils.UserEqual(user, share.Owner) || utils.UserEqual(user, share.Creator) { + // no filter we return earlier + if len(filters) == 0 { + ss = append(ss, share) + } else { + // check filters + // TODO(labkode): add the rest of filters. + for _, f := range filters { + if f.Type == ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID { + if utils.ResourceIDEqual(share.ResourceId, f.GetResourceId()) { + ss = append(ss, share) + } + } + } + } + } + } + return ss, nil +} + +func (m *mgr) StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) { + m.Lock() + defer m.Unlock() + + now := time.Now().UnixNano() + ts := &typespb.Timestamp{ + Seconds: uint64(now / 1000000000), + Nanos: uint32(now % 1000000000), + } + + share.Id = &ocm.ShareId{ + OpaqueId: genID(), + } + share.Ctime = ts + share.Mtime = ts + + m.model.ReceivedShares[share.Id.OpaqueId] = cloneReceivedShare(share) + + return share, nil +} + +func (m *mgr) ListReceivedShares(ctx context.Context, user *userpb.UserId) ([]*ocm.ReceivedShare, error) { + var rss []*ocm.ReceivedShare + m.Lock() + defer m.Unlock() + + for _, share := range m.model.ReceivedShares { + if utils.UserEqual(user, share.Owner) || utils.UserEqual(user, share.Creator) { + // omit shares created by me + continue + } + if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user, share.Grantee.GetUserId()) { + rss = append(rss, share) + } + } + return rss, nil +} + +func (m *mgr) GetReceivedShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) { + m.Lock() + defer m.Unlock() + + for _, share := range m.model.ReceivedShares { + if receivedShareEqual(ref, share) { + if share.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(user, share.Grantee.GetUserId()) { + return share, nil + } + } + } + return nil, errtypes.NotFound(ref.String()) +} + +func (m *mgr) UpdateReceivedShare(ctx context.Context, user *userpb.UserId, 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 { + return nil, err + } + + m.Lock() + defer m.Unlock() + + for _, mask := range fieldMask.Paths { + switch mask { + case "state": + rs.State = share.State + // TODO case "mount_point": + default: + return nil, errtypes.NotSupported("updating " + mask + " is not supported") + } + } + + if err := m.model.save(); err != nil { + return nil, errors.Wrap(err, "error saving model") + } + + return rs, nil +} diff --git a/pkg/ocm/share/repository/loader/loader.go b/pkg/ocm/share/repository/loader/loader.go new file mode 100644 index 00000000000..486f66c63e8 --- /dev/null +++ b/pkg/ocm/share/repository/loader/loader.go @@ -0,0 +1,26 @@ +// Copyright 2018-2023 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 loader + +import ( + // Load core share repository drivers. + _ "github.com/cs3org/reva/pkg/ocm/share/repository/json" + _ "github.com/cs3org/reva/pkg/ocm/share/repository/nextcloud" + // Add your own here. +) diff --git a/pkg/ocm/share/repository/registry/registry.go b/pkg/ocm/share/repository/registry/registry.go new file mode 100644 index 00000000000..c4a9b76b348 --- /dev/null +++ b/pkg/ocm/share/repository/registry/registry.go @@ -0,0 +1,34 @@ +// Copyright 2018-2023 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 registry + +import "github.com/cs3org/reva/pkg/ocm/share" + +// NewShareRepositoryFunc is the function that share repositories +// should register at init time. +type NewFunc func(map[string]interface{}) (share.Repository, error) + +// NewFuncs is a map containing all the registered share repositories. +var NewFuncs = map[string]NewFunc{} + +// Register registers a new share repository new function. +// Not safe for concurrent use. Safe for use from package init. +func Register(name string, f NewFunc) { + NewFuncs[name] = f +} diff --git a/pkg/ocm/share/share.go b/pkg/ocm/share/share.go index 4ab05bb639c..4d392b1b8a1 100644 --- a/pkg/ocm/share/share.go +++ b/pkg/ocm/share/share.go @@ -22,67 +22,40 @@ import ( "context" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "google.golang.org/genproto/protobuf/field_mask" ) -// Manager is the interface that manipulates the OCM shares. -type Manager interface { - // Create a new share in fn with the given acl. - Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string, - pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) - - // GetShare gets the information for a share by the given ref. - GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) - - // Unshare deletes the share pointed by ref. - Unshare(ctx context.Context, ref *ocm.ShareReference) error - - // UpdateShare updates the mode of the given share. - UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) - - // ListShares returns the shares created by the user. If md is provided is not nil, - // it returns only shares attached to the given resource. - ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) - - // ListReceivedShares returns the list of shares the user has access. - ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) - - // GetReceivedShare returns the information for a received share the user has access. - GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) - - // UpdateReceivedShare updates the received share with share state. - UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) -} - -// Repository is the interface that manipulates the OCM shares repositories. +// Repository is the interface that manipulates the OCM shares repository. type Repository interface { - // StoreShare stores a share. + // StoreShare // TODO StoreShare(ctx context.Context, share *ocm.Share) (*ocm.Share, error) // GetShare gets the information for a share by the given ref. - GetShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.Share, error) + GetShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) (*ocm.Share, error) // DeleteShare deletes the share pointed by ref. - DeleteShare(ctx context.Context, ref *ocm.ShareReference) error + DeleteShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) error // UpdateShare updates the mode of the given share. - UpdateShare(ctx context.Context, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) + UpdateShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference, p *ocm.SharePermissions) (*ocm.Share, error) // ListShares returns the shares created by the user. If md is provided is not nil, // it returns only shares attached to the given resource. - ListShares(ctx context.Context, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) + ListShares(ctx context.Context, user *userpb.UserId, filters []*ocm.ListOCMSharesRequest_Filter) ([]*ocm.Share, error) + + // StoreShare stores a share. + StoreReceivedShare(ctx context.Context, share *ocm.ReceivedShare) (*ocm.ReceivedShare, error) // ListReceivedShares returns the list of shares the user has access. - ListReceivedShares(ctx context.Context) ([]*ocm.ReceivedShare, error) + ListReceivedShares(ctx context.Context, user *userpb.UserId) ([]*ocm.ReceivedShare, error) // GetReceivedShare returns the information for a received share the user has access. - GetReceivedShare(ctx context.Context, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) + GetReceivedShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareReference) (*ocm.ReceivedShare, error) // UpdateReceivedShare updates the received share with share state. - UpdateReceivedShare(ctx context.Context, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) + UpdateReceivedShare(ctx context.Context, user *userpb.UserId, share *ocm.ReceivedShare, fieldMask *field_mask.FieldMask) (*ocm.ReceivedShare, error) } // ResourceIDFilter is an abstraction for creating filter by resource id. From 61df93c12cb7fe2ea303028428d48c583c218495 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Wed, 1 Feb 2023 16:46:47 +0100 Subject: [PATCH 06/44] rewrote ocm share provider --- .../ocmshareprovider/ocmshareprovider.go | 254 ++++++++++++------ 1 file changed, 170 insertions(+), 84 deletions(-) diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 51b99a3737e..402ee0481b9 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -20,13 +20,26 @@ package ocmshareprovider import ( "context" - + "fmt" + "path/filepath" + "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/ocmd" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/ocm/client" "github.com/cs3org/reva/pkg/ocm/share" "github.com/cs3org/reva/pkg/ocm/share/manager/registry" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/sharedconf" "github.com/mitchellh/mapstructure" "github.com/pkg/errors" "google.golang.org/grpc" @@ -37,26 +50,37 @@ func init() { } type config struct { - Driver string `mapstructure:"driver"` - Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + ClientTimeout int `mapstructure:"client_timeout"` + ClientInsecure bool `mapstructure:"client_insecure"` + GatewaySVC string `mapstructure:"gatewaysvc"` + WebDAVPrefix string `mapstructure:"webdav_prefix"` } type service struct { - conf *config - sm share.Manager + conf *config + repo share.Repository + client *client.OCMClient + gateway gateway.GatewayAPIClient } func (c *config) init() { if c.Driver == "" { c.Driver = "json" } + if c.ClientTimeout == 0 { + c.ClientTimeout = 10 + } + + c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) } func (s *service) Register(ss *grpc.Server) { ocm.RegisterOcmAPIServer(ss, s) } -func getShareManager(c *config) (share.Manager, error) { +func getShareRepository(c *config) (share.Repository, error) { if f, ok := registry.NewFuncs[c.Driver]; ok { return f(c.Drivers[c.Driver]) } @@ -80,14 +104,26 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { } c.init() - sm, err := getShareManager(c) + repo, err := getShareRepository(c) + if err != nil { + return nil, err + } + + client := client.New(&client.Config{ + Timeout: time.Duration(c.ClientTimeout) * time.Second, + Insecure: c.ClientInsecure, + }) + + gateway, err := pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySVC)) if err != nil { return nil, err } service := &service{ - conf: c, - sm: sm, + conf: c, + repo: repo, + client: client, + gateway: gateway, } return service, nil @@ -98,87 +134,123 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return []string{} + return nil } -// Note: this is for outgoing OCM shares -// This function is used when you for instance -// call `ocm-share-create` in reva-cli. -// For incoming OCM shares from internal/http/services/ocmd/shares.go -// there is the very similar but slightly different function -// CreateOCMCoreShare (the "Core" somehow means "incoming"). -// So make sure to keep in mind the difference between this file for outgoing: -// internal/grpc/services/ocmshareprovider/ocmshareprovider.go -// and the other one for incoming: -// internal/grpc/service/ocmcore/ocmcore.go -// Both functions end up calling the same s.sm.Share function -// on the OCM share manager: -// pkg/ocm/share/manager/{json|nextcloud|...}. -func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) { - if req.Opaque == nil { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, errtypes.BadRequest("can't find resource permissions"), ""), - }, nil +func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) { + for _, s := range originProvider.Services { + if s.Endpoint.Type.Name == "OCM" { + return s.Endpoint.Path, nil + } } + return "", errors.New("ocm endpoint not specified for mesh provider") +} - var permissions string - permOpaque, ok := req.Opaque.Map["permissions"] - if !ok { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, errtypes.BadRequest("resource permissions not set"), ""), - }, nil - } - switch permOpaque.Decoder { - case "plain": - permissions = string(permOpaque.Value) - default: - err := errtypes.NotSupported("opaque entry decoder not recognized: " + permOpaque.Decoder) - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, err, "invalid opaque entry decoder"), - }, nil +func formatOCMUser(u *userpb.UserId) string { + return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp) +} + +func getResourceType(info *providerv1beta1.ResourceInfo) string { + switch info.Type { + case providerv1beta1.ResourceType_RESOURCE_TYPE_FILE: + return "file" + case providerv1beta1.ResourceType_RESOURCE_TYPE_CONTAINER: + return "folder" } + return "unknown" +} - var name string - nameOpaque, ok := req.Opaque.Map["name"] - if !ok { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, errtypes.BadRequest("resource name not set"), ""), - }, nil +func (s *service) webdavURL(ctx context.Context, path string) string { + // the url is in the form of https://cernbox.cern.ch/remote.php/dav/files/gdelmont/eos/user/g/gdelmont + user := ctxpkg.ContextMustGetUser(ctx) + return filepath.Join(s.conf.WebDAVPrefix, user.Username, path) +} + +func (s *service) getWebdavProtocol(ctx context.Context, info *providerv1beta1.ResourceInfo, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { + var perms []string + if m.WebdavOptions.Permissions.InitiateFileDownload { + perms = append(perms, "read") } - switch nameOpaque.Decoder { - case "plain": - name = string(nameOpaque.Value) - default: - err := errtypes.NotSupported("opaque entry decoder not recognized: " + nameOpaque.Decoder) - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, err, "invalid opaque entry decoder"), - }, nil + if m.WebdavOptions.Permissions.InitiateFileUpload { + perms = append(perms, "write") } - // discover share type - sharetype := ocm.Share_SHARE_TYPE_REGULAR - protocol, ok := req.Opaque.Map["protocol"] - if ok { - switch protocol.Decoder { - case "plain": - if string(protocol.Value) == "datatx" { - sharetype = ocm.Share_SHARE_TYPE_TRANSFER - } - default: - err := errors.New("protocol decoder not recognized") - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, err, "error creating share"), - }, nil + return &ocmd.WebDAV{ + SharedSecret: ctxpkg.ContextMustGetToken(ctx), // TODO: change this and use an ocm token + Permissions: perms, + URL: s.webdavURL(ctx, info.Path), // TODO: change this and use an endpoint for ocm + } +} + +func (s *service) getProtocols(ctx context.Context, info *providerv1beta1.ResourceInfo, methods []*ocm.AccessMethod) ocmd.Protocols { + var p ocmd.Protocols + for _, m := range methods { + switch t := m.Term.(type) { + case *ocm.AccessMethod_WebdavOptions: + p = append(p, s.getWebdavProtocol(ctx, info, t)) + case *ocm.AccessMethod_WebappOptions: + // TODO + case *ocm.AccessMethod_DatatxOptions: + // TODO } } + return p +} + +func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) { + statRes, err := s.gateway.Stat(ctx, &providerv1beta1.StatRequest{ + Ref: &providerv1beta1.Reference{ + ResourceId: req.ResourceId, + }, + }) + if err != nil { + return nil, err + } + + if statRes.Status.Code != rpcv1beta1.Code_CODE_OK { + // TODO: review error codes + return nil, errtypes.InternalError(statRes.Status.Message) + } - var sharedSecret string - share, err := s.sm.Share(ctx, req.ResourceId, req.Grant, name, req.RecipientMeshProvider, permissions, nil, sharedSecret, sharetype) + info := statRes.Info + user := ctxpkg.ContextMustGetUser(ctx) + share := &ocm.Share{ + Name: filepath.Base(info.Path), + ResourceId: req.ResourceId, + Grantee: req.Grantee, + Owner: info.Owner, + Creator: user.Id, + AccessMethods: req.AccessMethods, + } + + share, err = s.repo.StoreShare(ctx, share) if err != nil { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, err, "error creating share: "+err.Error()), - }, nil + // TODO: err + return nil, errtypes.InternalError(err.Error()) + } + + ocmEndpoint, err := getOCMEndpoint(req.RecipientMeshProvider) + if err != nil { + // TODO: err + return nil, errtypes.InternalError(err.Error()) + } + + err = s.client.NewShare(ctx, ocmEndpoint, &client.NewShareRequest{ + ShareWith: req.Grantee.GetGroupId().OpaqueId, + Name: share.Name, + ResourceID: fmt.Sprintf("%s:%s", req.ResourceId.StorageId, req.ResourceId.OpaqueId), + Owner: formatOCMUser(info.Owner), + Sender: formatOCMUser(user.Id), + SenderDisplayName: user.DisplayName, + ShareType: "user", + ResourceType: getResourceType(info), + Protocols: s.getProtocols(ctx, info, req.AccessMethods), + }) + + if err != nil { + // TODO: err + return nil, errtypes.InternalError(err.Error()) } res := &ocm.CreateOCMShareResponse{ @@ -189,8 +261,11 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq } func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) { - err := s.sm.Unshare(ctx, req.Ref) - if err != nil { + // 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) + if err := s.repo.DeleteShare(ctx, user.Id, req.Ref); err != nil { + // TODO: error return &ocm.RemoveOCMShareResponse{ Status: status.NewInternal(ctx, err, "error removing share"), }, nil @@ -202,8 +277,10 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq } func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) { - share, err := s.sm.GetShare(ctx, req.Ref) + user := ctxpkg.ContextMustGetUser(ctx) + share, err := s.repo.GetShare(ctx, user.Id, req.Ref) if err != nil { + // TODO: error return &ocm.GetOCMShareResponse{ Status: status.NewInternal(ctx, err, "error getting share"), }, nil @@ -216,7 +293,8 @@ func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) } func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) { - shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager + user := ctxpkg.ContextMustGetUser(ctx) + shares, err := s.repo.ListShares(ctx, user.Id, req.Filters) if err != nil { return &ocm.ListOCMSharesResponse{ Status: status.NewInternal(ctx, err, "error listing shares"), @@ -231,8 +309,10 @@ func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesReque } func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) { - _, err := s.sm.UpdateShare(ctx, req.Ref, req.Field.GetPermissions()) // TODO(labkode): check what to update + user := ctxpkg.ContextMustGetUser(ctx) + _, err := s.repo.UpdateShare(ctx, user.Id, req.Ref, req.Field.GetPermissions()) // TODO(labkode): check what to update if err != nil { + // TODO: error return &ocm.UpdateOCMShareResponse{ Status: status.NewInternal(ctx, err, "error updating share"), }, nil @@ -245,8 +325,10 @@ func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareReq } func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) { - shares, err := s.sm.ListReceivedShares(ctx) + user := ctxpkg.ContextMustGetUser(ctx) + shares, err := s.repo.ListReceivedShares(ctx, user.Id) if err != nil { + // TODO: error return &ocm.ListReceivedOCMSharesResponse{ Status: status.NewInternal(ctx, err, "error listing received shares"), }, nil @@ -260,8 +342,10 @@ func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceiv } func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) { - _, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask) // TODO(labkode): check what to update + user := ctxpkg.ContextMustGetUser(ctx) + _, err := s.repo.UpdateReceivedShare(ctx, user.Id, req.Share, req.UpdateMask) // TODO(labkode): check what to update if err != nil { + // TODO: error return &ocm.UpdateReceivedOCMShareResponse{ Status: status.NewInternal(ctx, err, "error updating received share"), }, nil @@ -274,8 +358,10 @@ func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateRec } func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) { - share, err := s.sm.GetReceivedShare(ctx, req.Ref) + user := ctxpkg.ContextMustGetUser(ctx) + share, err := s.repo.GetReceivedShare(ctx, user.Id, req.Ref) if err != nil { + // TODO: error return &ocm.GetReceivedOCMShareResponse{ Status: status.NewInternal(ctx, err, "error getting received share"), }, nil From 75753ac7d1ac5d752a473654ddbecf5bc75a904c Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Wed, 1 Feb 2023 16:47:43 +0100 Subject: [PATCH 07/44] removed commented code --- pkg/ocm/share/repository/json/json.go | 135 -------------------------- 1 file changed, 135 deletions(-) diff --git a/pkg/ocm/share/repository/json/json.go b/pkg/ocm/share/repository/json/json.go index 57c15cc95d1..33b8faeeaff 100644 --- a/pkg/ocm/share/repository/json/json.go +++ b/pkg/ocm/share/repository/json/json.go @@ -223,141 +223,6 @@ func (m *mgr) GetShare(ctx context.Context, user *userpb.UserId, ref *ocm.ShareR return nil, errtypes.NotFound(ref.String()) } -// // Called from both grpc CreateOCMShare for outgoing -// // and http /ocm/shares for incoming -// // pi is provider info -// // pm is permissions. -// func (m *mgr) Share(ctx context.Context, md *provider.ResourceId, g *ocm.ShareGrant, name string, -// pi *ocmprovider.ProviderInfo, pm string, owner *userpb.UserId, token string, st ocm.Share_ShareType) (*ocm.Share, error) { -// id := genID() -// now := time.Now().UnixNano() -// ts := &typespb.Timestamp{ -// Seconds: uint64(now / 1000000000), -// Nanos: uint32(now % 1000000000), -// } - -// // Since both OCMCore and OCMShareProvider use the same package, we distinguish -// // between calls received from them on the basis of whether they provide info -// // about the remote provider on which the share is to be created. -// // If this info is provided, this call is on the owner's mesh provider and so -// // we call the CreateOCMCoreShare method on the remote provider as well, -// // else this is received from another provider and we only create a local share. -// var isOwnersMeshProvider bool -// if pi != nil { -// isOwnersMeshProvider = true -// } - -// var userID *userpb.UserId -// if !isOwnersMeshProvider { -// // Since this call is on the remote provider, the owner of the resource is expected to be specified. -// if owner == nil { -// return nil, errors.New("json: owner of resource not provided") -// } -// userID = owner -// g.Grantee.Opaque = &typespb.Opaque{ -// Map: map[string]*typespb.OpaqueEntry{ -// "token": { -// Decoder: "plain", -// Value: []byte(token), -// }, -// }, -// } -// } else { -// userID = ctxpkg.ContextMustGetUser(ctx).GetId() -// } - -// // do not allow share to myself if share is for a user -// if g.Grantee.Type == provider.GranteeType_GRANTEE_TYPE_USER && utils.UserEqual(g.Grantee.GetUserId(), userID) { -// return nil, errors.New("json: user and grantee are the same") -// } - -// // check if share already exists. -// key := &ocm.ShareKey{ -// Owner: userID, -// ResourceId: md, -// Grantee: g.Grantee, -// } -// _, err := m.getByKey(ctx, key) - -// // share already exists -// if isOwnersMeshProvider && err == nil { -// return nil, errtypes.AlreadyExists(key.String()) -// } - -// s := &ocm.Share{ -// Id: &ocm.ShareId{ -// OpaqueId: id, -// }, -// Name: name, -// ResourceId: md, -// Permissions: g.Permissions, -// Grantee: g.Grantee, -// Owner: userID, -// Creator: userID, -// Ctime: ts, -// Mtime: ts, -// ShareType: st, -// } - -// if isOwnersMeshProvider { -// protocol := map[string]interface{}{ -// "name": "webdav", -// "options": map[string]string{ -// "permissions": pm, -// "token": ctxpkg.ContextMustGetToken(ctx), -// }, -// } -// if st == ocm.Share_SHARE_TYPE_TRANSFER { -// protocol["name"] = "datatx" -// } - -// requestBodyMap := map[string]interface{}{ -// "shareWith": g.Grantee.GetUserId().OpaqueId, -// "name": name, -// "providerId": fmt.Sprintf("%s:%s", md.StorageId, md.OpaqueId), -// "owner": fmt.Sprintf("%s@%s", userID.OpaqueId, userID.Idp), -// "protocol": protocol, -// "meshProvider": userID.Idp, -// } -// err = sender.Send(ctx, requestBodyMap, pi) -// if err != nil { -// err = errors.Wrap(err, "error sending OCM POST") -// return nil, err -// } -// } - -// m.Lock() -// if err := m.model.ReadFile(); err != nil { -// err = errors.Wrap(err, "error reading model") -// return nil, err -// } - -// if isOwnersMeshProvider { -// encShare, err := utils.MarshalProtoV1ToJSON(s) -// if err != nil { -// return nil, err -// } -// m.model.Shares[s.Id.OpaqueId] = string(encShare) -// } else { -// encShare, err := utils.MarshalProtoV1ToJSON(&ocm.ReceivedShare{ -// Share: s, -// State: ocm.ShareState_SHARE_STATE_PENDING, -// }) -// if err != nil { -// return nil, err -// } -// m.model.ReceivedShares[s.Id.OpaqueId] = string(encShare) -// } - -// if err := m.model.Save(); err != nil { -// err = errors.Wrap(err, "error saving model") -// return nil, err -// } -// m.Unlock() - -// return s, nil -// } - func (m *mgr) getByID(ctx context.Context, id *ocm.ShareId) (*ocm.Share, error) { m.Lock() defer m.Unlock() From 4bef665a385964698da7566ffeffb4554b09060f Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte Date: Thu, 2 Feb 2023 11:56:43 +0100 Subject: [PATCH 08/44] fixed app provider with new cs3apis --- internal/grpc/services/gateway/appprovider.go | 2 +- pkg/app/app.go | 2 +- pkg/app/provider/demo/demo.go | 2 +- pkg/app/provider/wopi/wopi.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/grpc/services/gateway/appprovider.go b/internal/grpc/services/gateway/appprovider.go index 95ed8780091..a6b06bcbde0 100644 --- a/internal/grpc/services/gateway/appprovider.go +++ b/internal/grpc/services/gateway/appprovider.go @@ -195,7 +195,7 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour appProviderReq := &providerpb.OpenInAppRequest{ ResourceInfo: ri, - ViewMode: providerpb.OpenInAppRequest_ViewMode(req.ViewMode), + ViewMode: providerpb.ViewMode(req.ViewMode), AccessToken: accessToken, Opaque: req.Opaque, } diff --git a/pkg/app/app.go b/pkg/app/app.go index 32a4e34dbe1..da2720c15da 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -41,6 +41,6 @@ type Registry interface { // Provider is the interface that application providers implement // for interacting with external apps that serve the requested resource. type Provider interface { - GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token string, opaqueMap map[string]*typespb.OpaqueEntry, language string) (*appprovider.OpenInAppURL, error) + GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token string, opaqueMap map[string]*typespb.OpaqueEntry, language string) (*appprovider.OpenInAppURL, error) GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error) } diff --git a/pkg/app/provider/demo/demo.go b/pkg/app/provider/demo/demo.go index e10d7fa3c48..344f7d88833 100644 --- a/pkg/app/provider/demo/demo.go +++ b/pkg/app/provider/demo/demo.go @@ -40,7 +40,7 @@ type demoProvider struct { iframeUIProvider string } -func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token string, opaqueMap map[string]*typespb.OpaqueEntry, language string) (*appprovider.OpenInAppURL, error) { +func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token string, opaqueMap map[string]*typespb.OpaqueEntry, language string) (*appprovider.OpenInAppURL, error) { url := fmt.Sprintf("