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/go.mod b/go.mod index 9aa7188d1c..77ea1cf5d9 100644 --- a/go.mod +++ b/go.mod @@ -33,7 +33,7 @@ require ( github.com/google/uuid v1.3.0 github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 github.com/hashicorp/go-hclog v1.4.0 - github.com/hashicorp/go-plugin v1.4.4 + github.com/hashicorp/go-plugin v1.4.9 github.com/jedib0t/go-pretty v4.3.0+incompatible github.com/juliangruber/go-intersect v1.1.0 github.com/mattn/go-sqlite3 v1.14.10 diff --git a/go.sum b/go.sum index ed6a0e31d7..72842a0bfc 100644 --- a/go.sum +++ b/go.sum @@ -646,8 +646,8 @@ github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-msgpack v1.1.5 h1:9byZdVjKTe5mce63pRVNP1L7UAmdHOTEMGehn6KvJWs= github.com/hashicorp/go-msgpack v1.1.5/go.mod h1:gWVc3sv/wbDmR3rQsj1CAktEZzoz1YNK9NfGLXJ69/4= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= -github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= +github.com/hashicorp/go-plugin v1.4.9 h1:ESiK220/qE0aGxWdzKIvRH69iLiuN/PjoLTm69RoWtU= +github.com/hashicorp/go-plugin v1.4.9/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.6.6/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 56d1941c2c..87401ccecb 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"` ProviderDomain string `mapstructure:"provider_domain" docs:"The same domain registered in the provider authorizer"` WebDAVEndpoint string `mapstructure:"webdav_endpoint"` + 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 @@ -185,6 +198,16 @@ func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *oc } } +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) ocmd.Protocols { var p ocmd.Protocols for _, m := range share.AccessMethods { @@ -192,7 +215,7 @@ func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Proto case *ocm.AccessMethod_WebdavOptions: p = append(p, s.getWebdavProtocol(ctx, share, t)) case *ocm.AccessMethod_WebappOptions: - // TODO + p = append(p, s.getWebappProtocol(share)) case *ocm.AccessMethod_TransferOptions: // TODO } 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 }