From a83cc99f6734cf4c6956a98a098df5422deb6de3 Mon Sep 17 00:00:00 2001 From: Gianmaria Del Monte <39946305+gmgigi96@users.noreply.github.com> Date: Mon, 6 Mar 2023 18:23:22 +0100 Subject: [PATCH] Remote open in app (#3683) * implement open in app in sciencemesh * add webapp template * fix url creatioN * improved error message when share does not exist * add changelog --- changelog/unreleased/ocm-remote-apps.md | 3 + .../ocmshareprovider/ocmshareprovider.go | 45 ++++-- internal/http/services/sciencemesh/apps.go | 139 ++++++++++++++++++ .../http/services/sciencemesh/sciencemesh.go | 7 + 4 files changed, 183 insertions(+), 11 deletions(-) create mode 100644 changelog/unreleased/ocm-remote-apps.md create mode 100644 internal/http/services/sciencemesh/apps.go diff --git a/changelog/unreleased/ocm-remote-apps.md b/changelog/unreleased/ocm-remote-apps.md new file mode 100644 index 0000000000..e2d3bb07b4 --- /dev/null +++ b/changelog/unreleased/ocm-remote-apps.md @@ -0,0 +1,3 @@ +Enhancement: Remote open in app in OCM + +https://github.com/cs3org/reva/pull/3683 \ No newline at end of file diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index d8a537d3e8..192d2c1e3f 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -23,6 +23,8 @@ import ( "fmt" "net/url" "path/filepath" + "strings" + "text/template" "time" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -60,13 +62,15 @@ type config struct { GatewaySVC string `mapstructure:"gatewaysvc"` WebDAVPrefix string `mapstructure:"webdav_prefix"` ProviderDomain string `mapstructure:"provider_domain" docs:"The same domain registered in the provider authorizer"` + WebappTemplate string `mapstructure:"webapp_template"` } type service struct { - conf *config - repo share.Repository - client *client.OCMClient - gateway gateway.GatewayAPIClient + conf *config + repo share.Repository + client *client.OCMClient + gateway gateway.GatewayAPIClient + webappTmpl *template.Template } func (c *config) init() { @@ -76,6 +80,9 @@ func (c *config) init() { if c.ClientTimeout == 0 { c.ClientTimeout = 10 } + if c.WebappTemplate == "" { + c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}" + } c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) } @@ -123,11 +130,17 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { return nil, err } + tpl, err := template.New("webapp_template").Parse(c.WebappTemplate) + if err != nil { + return nil, err + } + service := &service{ - conf: c, - repo: repo, - client: client, - gateway: gateway, + conf: c, + repo: repo, + client: client, + gateway: gateway, + webappTmpl: tpl, } return service, nil @@ -190,14 +203,24 @@ func (s *service) getWebdavProtocol(ctx context.Context, info *providerpb.Resour } } -func (s *service) getProtocols(ctx context.Context, info *providerpb.ResourceInfo, methods []*ocm.AccessMethod) ocmd.Protocols { +func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp { + var b strings.Builder + if err := s.webappTmpl.Execute(&b, share); err != nil { + panic(err) + } + return &ocmd.Webapp{ + URITemplate: b.String(), + } +} + +func (s *service) getProtocols(ctx context.Context, share *ocm.Share, info *providerpb.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 + p = append(p, s.getWebappProtocol(share)) case *ocm.AccessMethod_TransferOptions: // TODO } @@ -285,7 +308,7 @@ func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareReq SenderDisplayName: user.DisplayName, ShareType: "user", ResourceType: getResourceType(info), - Protocols: s.getProtocols(ctx, info, req.AccessMethods), + Protocols: s.getProtocols(ctx, ocmshare, info, req.AccessMethods), } if req.Expiration != nil { diff --git a/internal/http/services/sciencemesh/apps.go b/internal/http/services/sciencemesh/apps.go new file mode 100644 index 0000000000..f256a3eb62 --- /dev/null +++ b/internal/http/services/sciencemesh/apps.go @@ -0,0 +1,139 @@ +// 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 sciencemesh + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + "github.com/cs3org/reva/internal/http/services/reqres" + "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/rhttp/router" +) + +type appsHandler struct { + gatewayClient gateway.GatewayAPIClient + ocmMountPoint string +} + +func (h *appsHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(pool.Endpoint(c.GatewaySvc)) + if err != nil { + return err + } + h.ocmMountPoint = c.OCMMountPoint + + return nil +} + +func (h *appsHandler) shareInfo(p string) (*ocmpb.ShareId, string) { + p = strings.TrimPrefix(p, h.ocmMountPoint) + shareID, rel := router.ShiftPath(p) + if len(rel) > 0 { + rel = rel[1:] + } + return &ocmpb.ShareId{OpaqueId: shareID}, rel +} + +func (h *appsHandler) OpenInApp(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if err := r.ParseForm(); err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "parameters could not be parsed", nil) + return + } + + path := r.Form.Get("file") + if path == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing file", nil) + return + } + + shareID, rel := h.shareInfo(path) + + template, err := h.webappTemplate(ctx, shareID) + if err != nil { + var e errtypes.NotFound + if errors.As(err, &e) { + reqres.WriteError(w, r, reqres.APIErrorNotFound, e.Error(), nil) + } + reqres.WriteError(w, r, reqres.APIErrorServerError, err.Error(), err) + return + } + + url := resolveTemplate(template, rel) + + if err := json.NewEncoder(w).Encode(map[string]any{ + "app_url": url, + }); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling JSON response", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (string, error) { + res, err := h.gatewayClient.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{ + Ref: &ocmpb.ShareReference{ + Spec: &ocmpb.ShareReference_Id{ + Id: id, + }, + }, + }) + if err != nil { + return "", err + } + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND { + return "", errtypes.NotFound(res.Status.Message) + } + return "", errtypes.InternalError(res.Status.Message) + } + + webapp, ok := getWebappProtocol(res.Share.Protocols) + if !ok { + return "", errtypes.BadRequest("share does not contain webapp protocol") + } + + return webapp.UriTemplate, nil +} + +func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) { + for _, p := range protocols { + if t, ok := p.Term.(*ocmpb.Protocol_WebappOptions); ok { + return t.WebappOptions, true + } + } + return nil, false +} + +func resolveTemplate(template string, rel string) string { + // the template is of type "https://open-cloud-mesh.org/s/share-hash/{relative-path-to-shared-resource}" + return strings.Replace(template, "{relative-path-to-shared-resource}", rel, 1) +} diff --git a/internal/http/services/sciencemesh/sciencemesh.go b/internal/http/services/sciencemesh/sciencemesh.go index ec9a235519..c1dd7946b1 100644 --- a/internal/http/services/sciencemesh/sciencemesh.go +++ b/internal/http/services/sciencemesh/sciencemesh.go @@ -69,6 +69,7 @@ type config struct { ProviderDomain string `mapstructure:"provider_domain"` SubjectTemplate string `mapstructure:"subject_template"` BodyTemplatePath string `mapstructure:"body_template_path"` + OCMMountPoint string `mapstructure:"ocm_mount_point"` } func (c *config) init() { @@ -94,11 +95,17 @@ func (s *svc) routerInit() error { return err } + appsHandler := new(appsHandler) + if err := appsHandler.init(s.conf); err != nil { + return err + } + s.router.Get("/generate-invite", tokenHandler.Generate) s.router.Get("/list-invite", tokenHandler.ListInvite) s.router.Post("/accept-invite", tokenHandler.AcceptInvite) s.router.Get("/find-accepted-users", tokenHandler.FindAccepted) s.router.Get("/list-providers", providersHandler.ListProviders) + s.router.Post("/open-in-app", appsHandler.OpenInApp) return nil }