From 91981bd6a09e9241fc1c37740b1796d7462f1f03 Mon Sep 17 00:00:00 2001 From: Ishank Arora Date: Mon, 26 Jul 2021 08:47:10 +0200 Subject: [PATCH] Extend app registry with AddProvider method and mimetype filters (#1785) --- changelog/unreleased/app-registry-refactor.md | 5 + cmd/revad/runtime/loader.go | 2 + examples/ocmd/ocmd-server-1.toml | 22 +- go.mod | 2 + go.sum | 20 +- .../grpc/services/appprovider/appprovider.go | 257 ++++----------- .../services/appprovider/appprovider_test.go | 35 +- .../grpc/services/appregistry/appregistry.go | 83 +++-- .../services/appregistry/appregistry_test.go | 108 +++--- internal/grpc/services/gateway/appprovider.go | 40 ++- internal/grpc/services/gateway/appregistry.go | 59 +++- .../http/services/appprovider/appprovider.go | 232 +++++++++++++ internal/http/services/ocmd/ocmd.go | 8 +- pkg/app/app.go | 20 +- pkg/app/provider/demo/demo.go | 27 +- pkg/app/provider/loader/loader.go | 26 ++ pkg/app/provider/registry/registry.go | 34 ++ pkg/app/provider/wopi/wopi.go | 309 ++++++++++++++++++ pkg/app/registry/loader/loader.go | 25 ++ pkg/app/registry/registry/registry.go | 34 ++ pkg/app/registry/static/static.go | 141 ++++++-- pkg/auth/manager/registry/registry.go | 2 +- pkg/auth/registry/registry/registry.go | 6 +- 23 files changed, 1140 insertions(+), 357 deletions(-) create mode 100644 changelog/unreleased/app-registry-refactor.md create mode 100644 internal/http/services/appprovider/appprovider.go create mode 100644 pkg/app/provider/loader/loader.go create mode 100644 pkg/app/provider/registry/registry.go create mode 100644 pkg/app/provider/wopi/wopi.go create mode 100644 pkg/app/registry/loader/loader.go create mode 100644 pkg/app/registry/registry/registry.go diff --git a/changelog/unreleased/app-registry-refactor.md b/changelog/unreleased/app-registry-refactor.md new file mode 100644 index 0000000000..bc3eaa842f --- /dev/null +++ b/changelog/unreleased/app-registry-refactor.md @@ -0,0 +1,5 @@ +Enhancement: Extend app registry with AddProvider method and mimetype filters + +https://github.com/cs3org/reva/pull/1785 +https://github.com/cs3org/cs3apis/pull/131 +https://github.com/cs3org/reva/issues/1779 diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index 0b90f93a6a..241f625bae 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -27,6 +27,8 @@ import ( _ "github.com/cs3org/reva/internal/http/interceptors/auth/tokenwriter/loader" _ "github.com/cs3org/reva/internal/http/interceptors/loader" _ "github.com/cs3org/reva/internal/http/services/loader" + _ "github.com/cs3org/reva/pkg/app/provider/loader" + _ "github.com/cs3org/reva/pkg/app/registry/loader" _ "github.com/cs3org/reva/pkg/appauth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/manager/loader" _ "github.com/cs3org/reva/pkg/auth/registry/loader" diff --git a/examples/ocmd/ocmd-server-1.toml b/examples/ocmd/ocmd-server-1.toml index 62b13d83a0..9d69b7ccc7 100644 --- a/examples/ocmd/ocmd-server-1.toml +++ b/examples/ocmd/ocmd-server-1.toml @@ -76,22 +76,18 @@ providers = "providers.demo.json" [grpc.services.publicshareprovider] driver = "memory" -[grpc.services.appprovider] -driver = "demo" -iopsecret = "testsecret" -wopiurl = "http://0.0.0.0:8880/" -wopibridgeurl = "http://localhost:8000/wopib" - [grpc.services.appregistry] driver = "static" -[grpc.services.appregistry.static.rules] -"text/plain" = "localhost:19000" -"text/markdown" = "localhost:19000" -"application/compressed-markdown" = "localhost:19000" -"application/vnd.oasis.opendocument.text" = "localhost:19000" -"application/vnd.oasis.opendocument.spreadsheet" = "localhost:19000" -"application/vnd.oasis.opendocument.presentation" = "localhost:19000" +[grpc.services.appprovider] +driver = "demo" +app_provider_url = "localhost:19000" + +[grpc.services.appprovider.drivers.wopi] +iop_secret = "hello" +wopi_url = "http://0.0.0.0:8880/" +app_name = "Collabora" +app_url = "https://your-collabora-server.org:9980" [grpc.services.storageprovider] driver = "localhome" diff --git a/go.mod b/go.mod index db20d8604a..792308a3ed 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/ReneKroon/ttlcache/v2 v2.7.0 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect github.com/aws/aws-sdk-go v1.40.0 + github.com/beevik/etree v1.1.0 github.com/bluele/gcache v0.0.2 github.com/c-bata/go-prompt v0.2.5 github.com/cheggaaa/pb v1.0.29 @@ -68,6 +69,7 @@ require ( go 1.16 replace ( + github.com/cs3org/go-cs3apis => github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479 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 google.golang.org/grpc => google.golang.org/grpc v1.26.0 // temporary downgrade diff --git a/go.sum b/go.sum index 64681a9892..9935ce0633 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.40.0 h1:nTCSQAeahNt15SOYxuDwJ8XvMhOU3Uqe7eJUPv7+Vsk= github.com/aws/aws-sdk-go v1.40.0/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs= +github.com/beevik/etree v1.1.0/go.mod h1:r8Aw8JqVegEf0w2fDnATrX9VpkMcyFeM0FhwO62wh+A= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -104,8 +106,6 @@ github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7Do github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJffz4pz0o1WuQxJ28+5x5JgaHD8= github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= -github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f h1:l09QSEPO8DI3V2hBnc6KhTsrNg/DTyBYjCTwSb/HR6Q= -github.com/cs3org/go-cs3apis v0.0.0-20210702091910-85a56bfd027f/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -206,6 +206,7 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -266,9 +267,13 @@ github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoP github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.4.2 h1:yFvG3ufXXpqiMiZx9HLcaK3XbIqQ1WJFR/F1a2CuVw0= +github.com/hashicorp/go-plugin v1.4.2/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -284,6 +289,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.0 h1:gvV6jG9dTgFEncxo+AF7PH6MZXi/vZl25owA/8Dg8Wo= github.com/huandu/xstrings v1.3.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -293,8 +300,12 @@ github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ= github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479 h1:xB9tNEbbRzAMuE2IVVq/aTi9VKUHWbfm+/f2+08w4L0= +github.com/ishank011/go-cs3apis v0.0.0-20210715114809-34729f68a479/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= @@ -375,6 +386,8 @@ github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFW github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -408,6 +421,7 @@ github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -624,6 +638,7 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -814,6 +829,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/internal/grpc/services/appprovider/appprovider.go b/internal/grpc/services/appprovider/appprovider.go index b7545f7927..fe2479d433 100644 --- a/internal/grpc/services/appprovider/appprovider.go +++ b/internal/grpc/services/appprovider/appprovider.go @@ -19,28 +19,23 @@ package appprovider import ( - "bytes" "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/url" "os" - "path" - "strings" "time" providerpb "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" "github.com/cs3org/reva/pkg/app" - "github.com/cs3org/reva/pkg/app/provider/demo" - "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/app/provider/registry" "github.com/cs3org/reva/pkg/errtypes" + "github.com/cs3org/reva/pkg/logger" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" - "github.com/cs3org/reva/pkg/rhttp" - "github.com/cs3org/reva/pkg/user" + "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" ) @@ -50,16 +45,31 @@ func init() { type service struct { provider app.Provider - client *http.Client conf *config } type config struct { - Driver string `mapstructure:"driver"` - Demo map[string]interface{} `mapstructure:"demo"` - IopSecret string `mapstructure:"iopsecret" docs:";The iopsecret used to connect to the wopiserver."` - WopiURL string `mapstructure:"wopiurl" docs:";The wopiserver's URL."` - WopiBrURL string `mapstructure:"wopibridgeurl" docs:";The wopibridge's URL."` + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + AppProviderURL string `mapstructure:"app_provider_url"` + GatewaySvc string `mapstructure:"gatewaysvc"` +} + +func (c *config) init() { + if c.Driver == "" { + c.Driver = "demo" + } + c.AppProviderURL = sharedconf.GetGatewaySVC(c.AppProviderURL) + c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + return nil, err + } + c.init() + return c, nil } // New creates a new AppProviderService @@ -77,20 +87,40 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { service := &service{ conf: c, provider: provider, - client: rhttp.GetHTTPClient( - rhttp.Timeout(5 * time.Second), - ), } + go service.registerProvider() return service, nil } -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} - if err := mapstructure.Decode(m, c); err != nil { - return nil, err +func (s *service) registerProvider() { + // Give the appregistry service time to come up + time.Sleep(2 * time.Second) + + ctx := context.Background() + log := logger.New().With().Int("pid", os.Getpid()).Logger() + pInfo, err := s.provider.GetAppProviderInfo(ctx) + if err != nil { + log.Error().Err(err).Msgf("error registering app provider: could not get provider info") + return + } + pInfo.Address = s.conf.AppProviderURL + + client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) + if err != nil { + log.Error().Err(err).Msgf("error registering app provider: could not get gateway client") + return + } + res, err := client.AddAppProvider(ctx, ®istrypb.AddAppProviderRequest{Provider: pInfo}) + if err != nil { + log.Error().Err(err).Msgf("error registering app provider: error calling add app provider") + return + } + if res.Status.Code != rpc.Code_CODE_OK { + err = status.NewErrorFromCode(res.Status.Code, "appprovider") + log.Error().Err(err).Msgf("error registering app provider: add app provider returned error") + return } - return c, nil } func (s *service) Close() error { @@ -106,182 +136,27 @@ func (s *service) Register(ss *grpc.Server) { } func getProvider(c *config) (app.Provider, error) { - switch c.Driver { - case "demo": - return demo.New(c.Demo) - default: - return nil, errtypes.NotFound("driver not found: " + c.Driver) + if f, ok := registry.NewFuncs[c.Driver]; ok { + return f(c.Drivers[c.Driver]) } -} - -func (s *service) getWopiAppEndpoints(ctx context.Context) (map[string]interface{}, error) { - // TODO this query will eventually be served by Reva. - // For the time being it is a remnant of the CERNBox-specific WOPI server, which justifies the /cbox path in the URL. - wopiurl, err := url.Parse(s.conf.WopiURL) - if err != nil { - return nil, err - } - wopiurl.Path = path.Join(wopiurl.Path, "/wopi/cbox/endpoints") - appsReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil) - if err != nil { - return nil, err - } - appsRes, err := s.client.Do(appsReq) - if err != nil { - return nil, err - } - defer appsRes.Body.Close() - if appsRes.StatusCode != http.StatusOK { - return nil, errtypes.InternalError(fmt.Sprintf("Request to WOPI server returned %d", appsRes.StatusCode)) - } - appsBody, err := ioutil.ReadAll(appsRes.Body) - if err != nil { - return nil, err - } - - appsURLMap := make(map[string]interface{}) - err = json.Unmarshal(appsBody, &appsURLMap) - if err != nil { - return nil, err - } - - log := appctx.GetLogger(ctx) - log.Info().Msg(fmt.Sprintf("Successfully retrieved %d WOPI app endpoints", len(appsURLMap))) - return appsURLMap, nil + return nil, errtypes.NotFound("driver not found: " + c.Driver) } func (s *service) OpenInApp(ctx context.Context, req *providerpb.OpenInAppRequest) (*providerpb.OpenInAppResponse, error) { - - log := appctx.GetLogger(ctx) - - wopiurl, err := url.Parse(s.conf.WopiURL) - if err != nil { - return nil, err - } - wopiurl.Path = path.Join(wopiurl.Path, "/wopi/iop/open") - httpReq, err := rhttp.NewRequest(ctx, "GET", wopiurl.String(), nil) + appURL, err := s.provider.GetAppURL(ctx, req.ResourceInfo, req.ViewMode, req.AccessToken) if err != nil { - return nil, err - } - - q := httpReq.URL.Query() - q.Add("fileid", req.ResourceInfo.GetId().OpaqueId) - q.Add("endpoint", req.ResourceInfo.GetId().StorageId) - q.Add("viewmode", req.ViewMode.String()) - // TODO the folder URL should be resolved as e.g. `'https://cernbox.cern.ch/index.php/apps/files/?dir=' + filepath.Dir(req.Ref.GetPath())` - // or should be deprecated/removed altogether, needs discussion and decision. - q.Add("folderurl", "undefined") - u, ok := user.ContextGetUser(ctx) - if ok { - q.Add("username", u.Username) - } - // else defaults to "Anonymous Guest" - - if s.conf.IopSecret == "" { - s.conf.IopSecret = os.Getenv("REVA_APPPROVIDER_IOPSECRET") - } - - httpReq.Header.Set("Authorization", "Bearer "+s.conf.IopSecret) - httpReq.Header.Set("TokenHeader", req.AccessToken) - - httpReq.URL.RawQuery = q.Encode() - - openRes, err := s.client.Do(httpReq) - - if err != nil { - res := &providerpb.OpenInAppResponse{ - Status: status.NewInternal(ctx, err, "appprovider: error performing open request to WOPI"), - } - return res, nil - } - defer openRes.Body.Close() - - if openRes.StatusCode != http.StatusOK { + err := errors.Wrap(err, "appprovider: error calling GetAppURL") res := &providerpb.OpenInAppResponse{ - Status: status.NewInvalid(ctx, fmt.Sprintf("appprovider: error performing open request to WOPI, status code: %d", openRes.StatusCode)), + Status: status.NewInternal(ctx, err, "error getting app URL"), } return res, nil } - - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(openRes.Body) - if err != nil { - return nil, err - } - openResBody := buf.String() - - var viewmode string - if req.ViewMode == providerpb.OpenInAppRequest_VIEW_MODE_READ_WRITE { - viewmode = "edit" - } else { - viewmode = "view" - } - - var appProviderURL string - if req.App == "" { - // Default behavior: work out the application URL to be used for this file - // TODO call this e.g. once a day or a week, and cache the content in a shared map protected by a multi-reader Lock - appsURLMap, err := s.getWopiAppEndpoints(ctx) - if err != nil { - res := &providerpb.OpenInAppResponse{ - Status: status.NewInternal(ctx, err, "appprovider: getWopiAppEndpoints failed"), - } - return res, nil - } - viewOptions := appsURLMap[path.Ext(req.ResourceInfo.GetPath())] - viewOptionsMap, ok := viewOptions.(map[string]interface{}) - if !ok { - res := &providerpb.OpenInAppResponse{ - Status: status.NewInvalid(ctx, "Incorrect parsing of the App URLs map from the WOPI server"), - } - return res, nil - } - - appProviderURL = fmt.Sprintf("%v", viewOptionsMap[viewmode]) - if strings.Contains(appProviderURL, "?") { - appProviderURL += "&" - } else { - appProviderURL += "?" - } - appProviderURL = fmt.Sprintf("%sWOPISrc=%s", appProviderURL, openResBody) - } else { - // User specified the application to use, generate the URL out of that - // TODO map the given req.App to the URL via config. For now assume it's a URL! - appProviderURL = fmt.Sprintf("%sWOPISrc=%s", req.App, openResBody) - } - - // In case of applications served by the WOPI bridge, resolve the URL and go to the app - // Note that URL matching is performed via string matching, not via IP resolution: may need to fix this - if len(s.conf.WopiBrURL) > 0 && strings.Contains(appProviderURL, s.conf.WopiBrURL) { - httpClient := rhttp.GetHTTPClient( - rhttp.Context(ctx), - rhttp.Timeout(time.Duration(5*int64(time.Second))), - ) - httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { - // do not follow a redirect - return http.ErrUseLastResponse - } - - bridgeReq, err := rhttp.NewRequest(ctx, "GET", appProviderURL, nil) - if err != nil { - return nil, err - } - bridgeRes, err := httpClient.Do(bridgeReq) - if err != nil { - return nil, err - } - defer bridgeRes.Body.Close() - if bridgeRes.StatusCode != http.StatusFound { - return nil, errtypes.InternalError(fmt.Sprintf("Request to WOPI bridge returned %d", bridgeRes.StatusCode)) - } - appProviderURL = bridgeRes.Header.Get("Location") + res := &providerpb.OpenInAppResponse{ + Status: status.NewOK(ctx), + AppUrl: appURL, } + return res, nil - log.Info().Msg(fmt.Sprintf("Returning app provider URL %s", appProviderURL)) - return &providerpb.OpenInAppResponse{ - Status: status.NewOK(ctx), - AppUrl: appProviderURL, - }, nil } func (s *service) OpenFileInAppProvider(ctx context.Context, req *providerpb.OpenFileInAppProviderRequest) (*providerpb.OpenFileInAppProviderResponse, error) { diff --git a/internal/grpc/services/appprovider/appprovider_test.go b/internal/grpc/services/appprovider/appprovider_test.go index 14b83d0d7c..4d76286565 100644 --- a/internal/grpc/services/appprovider/appprovider_test.go +++ b/internal/grpc/services/appprovider/appprovider_test.go @@ -34,29 +34,36 @@ func Test_parseConfig(t *testing.T) { wantErr interface{} }{ { - name: "all configurations set", + name: "all configurations set for demo driver", m: map[string]interface{}{ - "Driver": "demo", - "Demo": map[string]interface{}{"a": "b", "c": "d"}, - "IopSecret": "very-secret", - "WopiURL": "https://my.wopi:9871", + "Driver": "demo", + "Drivers": map[string]map[string]interface{}{"demo": map[string]interface{}{"a": "b", "c": "d"}}, }, want: &config{ - Driver: "demo", - Demo: map[string]interface{}{"a": "b", "c": "d"}, - IopSecret: "very-secret", - WopiURL: "https://my.wopi:9871", + Driver: "demo", + Drivers: map[string]map[string]interface{}{"demo": map[string]interface{}{"a": "b", "c": "d"}}, + }, + wantErr: nil, + }, + { + name: "all configurations set for wopi driver", + m: map[string]interface{}{ + "Driver": "wopi", + "Drivers": map[string]map[string]interface{}{"wopi": map[string]interface{}{"iop_secret": "very-secret", "wopi_url": "https://my.wopi:9871"}}, + }, + want: &config{ + Driver: "wopi", + Drivers: map[string]map[string]interface{}{"wopi": map[string]interface{}{"iop_secret": "very-secret", "wopi_url": "https://my.wopi:9871"}}, }, wantErr: nil, }, { name: "wrong type of setting", - m: map[string]interface{}{"Driver": 123, "IopSecret": 456}, + m: map[string]interface{}{"Driver": 123, "NonExistentField": 456}, want: nil, wantErr: &mapstructure.Error{ Errors: []string{ "'driver' expected type 'string', got unconvertible type 'int', value: '123'", - "'iopsecret' expected type 'string', got unconvertible type 'int', value: '456'", }, }, }, @@ -64,10 +71,8 @@ func Test_parseConfig(t *testing.T) { name: "undefined settings type", m: map[string]interface{}{"Not-Defined": 123}, want: &config{ - Driver: "", - Demo: map[string]interface{}(nil), - IopSecret: "", - WopiURL: "", + Driver: "demo", + Drivers: map[string]map[string]interface{}(nil), }, wantErr: nil, }, diff --git a/internal/grpc/services/appregistry/appregistry.go b/internal/grpc/services/appregistry/appregistry.go index 3f2f99db6c..5fa7ed6f76 100644 --- a/internal/grpc/services/appregistry/appregistry.go +++ b/internal/grpc/services/appregistry/appregistry.go @@ -25,7 +25,7 @@ import ( registrypb "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" "github.com/cs3org/reva/pkg/app" - "github.com/cs3org/reva/pkg/app/registry/static" + "github.com/cs3org/reva/pkg/app/registry/registry" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/rgrpc" "github.com/cs3org/reva/pkg/rgrpc/status" @@ -37,7 +37,7 @@ func init() { } type svc struct { - registry app.Registry + reg app.Registry } func (s *svc) Close() error { @@ -45,7 +45,7 @@ func (s *svc) Close() error { } func (s *svc) UnprotectedEndpoints() []string { - return []string{} + return []string{"/cs3.app.registry.v1beta1.RegistryAPI/AddAppProvider"} } func (s *svc) Register(ss *grpc.Server) { @@ -53,8 +53,14 @@ func (s *svc) Register(ss *grpc.Server) { } type config struct { - Driver string `mapstructure:"driver"` - Static map[string]interface{} `mapstructure:"static"` + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` +} + +func (c *config) init() { + if c.Driver == "" { + c.Driver = "static" + } } // New creates a new StorageRegistryService @@ -65,13 +71,13 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { return nil, err } - registry, err := getRegistry(c) + reg, err := getRegistry(c) if err != nil { return nil, err } svc := &svc{ - registry: registry, + reg: reg, } return svc, nil @@ -82,45 +88,53 @@ func parseConfig(m map[string]interface{}) (*config, error) { if err := mapstructure.Decode(m, c); err != nil { return nil, err } + c.init() return c, nil } func getRegistry(c *config) (app.Registry, error) { - switch c.Driver { - case "static": - return static.New(c.Static) - default: - return nil, errtypes.NotFound("driver not found: " + c.Driver) + if f, ok := registry.NewFuncs[c.Driver]; ok { + return f(c.Drivers[c.Driver]) } + return nil, errtypes.NotFound("appregistrysvc: driver not found: " + c.Driver) } func (s *svc) GetAppProviders(ctx context.Context, req *registrypb.GetAppProvidersRequest) (*registrypb.GetAppProvidersResponse, error) { - p, err := s.registry.FindProvider(ctx, req.ResourceInfo.MimeType) + p, err := s.reg.FindProviders(ctx, req.ResourceInfo.MimeType) if err != nil { return ®istrypb.GetAppProvidersResponse{ Status: status.NewInternal(ctx, err, "error looking for the app provider"), }, nil } - provider := format(p) res := ®istrypb.GetAppProvidersResponse{ Status: status.NewOK(ctx), - Providers: []*registrypb.ProviderInfo{provider}, + Providers: p, + } + return res, nil +} + +func (s *svc) AddAppProvider(ctx context.Context, req *registrypb.AddAppProviderRequest) (*registrypb.AddAppProviderResponse, error) { + err := s.reg.AddProvider(ctx, req.Provider) + if err != nil { + return ®istrypb.AddAppProviderResponse{ + Status: status.NewInternal(ctx, err, "error adding the app provider"), + }, nil + } + + res := ®istrypb.AddAppProviderResponse{ + Status: status.NewOK(ctx), } return res, nil } func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvidersRequest) (*registrypb.ListAppProvidersResponse, error) { - pvds, err := s.registry.ListProviders(ctx) + providers, err := s.reg.ListProviders(ctx) if err != nil { return ®istrypb.ListAppProvidersResponse{ Status: status.NewInternal(ctx, err, "error listing the app providers"), }, nil } - providers := make([]*registrypb.ProviderInfo, 0, len(pvds)) - for _, pvd := range pvds { - providers = append(providers, format(pvd)) - } res := ®istrypb.ListAppProvidersResponse{ Status: status.NewOK(ctx), @@ -129,8 +143,31 @@ func (s *svc) ListAppProviders(ctx context.Context, req *registrypb.ListAppProvi return res, nil } -func format(p *app.ProviderInfo) *registrypb.ProviderInfo { - return ®istrypb.ProviderInfo{ - Address: p.Location, +func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.GetDefaultAppProviderForMimeTypeRequest) (*registrypb.GetDefaultAppProviderForMimeTypeResponse, error) { + provider, err := s.reg.GetDefaultProviderForMimeType(ctx, req.MimeType) + if err != nil { + return ®istrypb.GetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewInternal(ctx, err, "error getting the default app provider for the mimetype"), + }, nil + } + + res := ®istrypb.GetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewOK(ctx), + Provider: provider, + } + return res, nil +} + +func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registrypb.SetDefaultAppProviderForMimeTypeRequest) (*registrypb.SetDefaultAppProviderForMimeTypeResponse, error) { + err := s.reg.SetDefaultProviderForMimeType(ctx, req.MimeType, req.Provider) + if err != nil { + return ®istrypb.SetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewInternal(ctx, err, "error setting the default app provider for the mimetype"), + }, nil } + + res := ®istrypb.SetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewOK(ctx), + } + return res, nil } diff --git a/internal/grpc/services/appregistry/appregistry_test.go b/internal/grpc/services/appregistry/appregistry_test.go index b2c7cb4e9c..684a1c3ce6 100644 --- a/internal/grpc/services/appregistry/appregistry_test.go +++ b/internal/grpc/services/appregistry/appregistry_test.go @@ -38,15 +38,21 @@ func (a ByAddress) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func Test_ListAppProviders(t *testing.T) { tests := []struct { - name string - rules map[string]interface{} - want *registrypb.ListAppProvidersResponse + name string + providers map[string]interface{} + want *registrypb.ListAppProvidersResponse }{ { name: "simple test", - rules: map[string]interface{}{ - "text/json": "some Address", - "currently/ignored": "an other address", + providers: map[string]interface{}{ + "some Address": map[string]interface{}{ + "address": "some Address", + "mimetypes": []string{"text/json"}, + }, + "another address": map[string]interface{}{ + "address": "another address", + "mimetypes": []string{"currently/ignored"}, + }, }, // only Status and Providers will be asserted in the tests @@ -57,27 +63,36 @@ func Test_ListAppProviders(t *testing.T) { Message: "", }, Providers: []*registrypb.ProviderInfo{ - {Address: "some Address"}, - {Address: "an other address"}, + { + Address: "some Address", + MimeTypes: []string{"text/json"}, + }, + { + Address: "another address", + MimeTypes: []string{"currently/ignored"}, + }, }, }, }, { - name: "rules is nil", - rules: nil, + name: "providers is nil", + providers: nil, want: ®istrypb.ListAppProvidersResponse{ Status: &rpcv1beta1.Status{ Code: 1, Trace: "00000000000000000000000000000000", }, Providers: []*registrypb.ProviderInfo{ - {Address: ""}, + { + Address: "", + MimeTypes: []string{"text/plain"}, + }, }, }, }, { - name: "empty rules", - rules: map[string]interface{}{}, + name: "empty providers", + providers: map[string]interface{}{}, // only Status and Providers will be asserted in the tests want: ®istrypb.ListAppProvidersResponse{ @@ -87,14 +102,17 @@ func Test_ListAppProviders(t *testing.T) { Message: "", }, Providers: []*registrypb.ProviderInfo{ - {Address: ""}, + { + Address: "", + MimeTypes: []string{"text/plain"}, + }, }, }, }, { - name: "rule value is nil", - rules: map[string]interface{}{ - "text/json": nil, + name: "provider value is nil", + providers: map[string]interface{}{ + "some Address": nil, }, // only Status and Providers will be asserted in the tests @@ -104,23 +122,21 @@ func Test_ListAppProviders(t *testing.T) { Trace: "00000000000000000000000000000000", Message: "", }, - Providers: []*registrypb.ProviderInfo{ - {Address: ""}, - }, + Providers: []*registrypb.ProviderInfo{nil}, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rr, err := static.New(map[string]interface{}{"Rules": tt.rules}) + rr, err := static.New(map[string]interface{}{"Providers": tt.providers}) if err != nil { t.Errorf("could not create registry error = %v", err) return } ss := &svc{ - registry: rr, + reg: rr, } got, err := ss.ListAppProviders(context.Background(), nil) @@ -137,13 +153,19 @@ func Test_ListAppProviders(t *testing.T) { } func Test_GetAppProviders(t *testing.T) { - rules := map[string]interface{}{ - "text/json": "JSON format", - "image/bmp": "Windows OS/2 Bitmap Graphics", - "application/vnd.openxmlformats-officedocument.wordprocessingml.document": "Microsoft Word (OpenXML)", - "application/vnd.oasis.opendocument.presentation": "OpenDocument presentation document", - "application/vnd.apple.installer+xml": "Apple Installer Package", - "text/xml": "XML", + providers := map[string]interface{}{ + "text appprovider addr": map[string]interface{}{ + "address": "text appprovider addr", + "mimetypes": []string{"text/json", "text/xml"}, + }, + "image appprovider addr": map[string]interface{}{ + "address": "image appprovider addr", + "mimetypes": []string{"image/bmp"}, + }, + "misc appprovider addr": map[string]interface{}{ + "address": "misc appprovider addr", + "mimetypes": []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.presentation", "application/vnd.apple.installer+xml"}, + }, } tests := []struct { @@ -162,7 +184,10 @@ func Test_GetAppProviders(t *testing.T) { Message: "", }, Providers: []*registrypb.ProviderInfo{ - {Address: "JSON format"}, + { + Address: "text appprovider addr", + MimeTypes: []string{"text/json", "text/xml"}, + }, }, }, }, @@ -176,7 +201,10 @@ func Test_GetAppProviders(t *testing.T) { Message: "", }, Providers: []*registrypb.ProviderInfo{ - {Address: "Apple Installer Package"}, + { + Address: "misc appprovider addr", + MimeTypes: []string{"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.oasis.opendocument.presentation", "application/vnd.apple.installer+xml"}, + }, }, }, }, @@ -230,14 +258,14 @@ func Test_GetAppProviders(t *testing.T) { }, } - rr, err := static.New(map[string]interface{}{"Rules": rules}) + rr, err := static.New(map[string]interface{}{"providers": providers}) if err != nil { t.Errorf("could not create registry error = %v", err) return } ss := &svc{ - registry: rr, + reg: rr, } for _, tt := range tests { @@ -260,11 +288,11 @@ func Test_GetAppProviders(t *testing.T) { func TestNew(t *testing.T) { tests := []struct { - name string - m map[string]interface{} - rules map[string]interface{} - want svc - wantErr interface{} + name string + m map[string]interface{} + providers map[string]interface{} + want svc + wantErr interface{} }{ { name: "no error", @@ -274,12 +302,12 @@ func TestNew(t *testing.T) { { name: "not existing driver", m: map[string]interface{}{"Driver": "doesnotexist"}, - wantErr: "error: not found: driver not found: doesnotexist", + wantErr: "error: not found: appregistrysvc: driver not found: doesnotexist", }, { name: "empty", m: map[string]interface{}{}, - wantErr: "error: not found: driver not found: ", + wantErr: nil, }, { name: "extra not existing field in setting", diff --git a/internal/grpc/services/gateway/appprovider.go b/internal/grpc/services/gateway/appprovider.go index f7921c9520..304e36f038 100644 --- a/internal/grpc/services/gateway/appprovider.go +++ b/internal/grpc/services/gateway/appprovider.go @@ -183,7 +183,7 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour }, nil } - provider, err := s.findAppProvider(ctx, ri) + provider, err := s.findAppProvider(ctx, ri, app) if err != nil { err = errors.Wrap(err, "gateway: error calling findAppProvider") var st *rpc.Status @@ -208,7 +208,6 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour appProviderReq := &providerpb.OpenInAppRequest{ ResourceInfo: ri, ViewMode: providerpb.OpenInAppRequest_ViewMode(vm), - App: app, AccessToken: accessToken, } @@ -220,32 +219,49 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour return res, nil } -func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo) (*registry.ProviderInfo, error) { +func (s *svc) findAppProvider(ctx context.Context, ri *storageprovider.ResourceInfo, app string) (*registry.ProviderInfo, error) { c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint) if err != nil { err = errors.Wrap(err, "gateway: error getting appregistry client") return nil, err } + + if app == "" { + // We need to get the default provider in case app is not set + // If the default isn't set as well, we'll return the first provider which matches the mimetype + res, err := c.GetDefaultAppProviderForMimeType(ctx, ®istry.GetDefaultAppProviderForMimeTypeRequest{ + MimeType: ri.MimeType, + }) + if err == nil && res.Status.Code == rpc.Code_CODE_OK && res.Provider != nil { + return res.Provider, nil + } + } + res, err := c.GetAppProviders(ctx, ®istry.GetAppProvidersRequest{ ResourceInfo: ri, }) - if err != nil { err = errors.Wrap(err, "gateway: error calling GetAppProviders") return nil, err } - - // TODO(labkode): when sending an Open to the proxy we need to choose one - // provider from the list of available as the client - if res.Status.Code == rpc.Code_CODE_OK { - return res.Providers[0], nil + if res.Status.Code != rpc.Code_CODE_OK { + if res.Status.Code == rpc.Code_CODE_NOT_FOUND { + return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String()) + } + return nil, errtypes.InternalError("gateway: error finding app providers") } - if res.Status.Code == rpc.Code_CODE_NOT_FOUND { - return nil, errtypes.NotFound("gateway: app provider not found for resource: " + ri.String()) + if app != "" { + for _, p := range res.Providers { + if p.Name == app { + return p, nil + } + } + return nil, errtypes.NotFound("gateway: app provider not found: " + app) } - return nil, errtypes.InternalError("gateway: error finding a storage provider") + // As a fallback, return the first provider in the list + return res.Providers[0], nil } func getGRPCConfig(opaque *typespb.Opaque) (bool, bool) { diff --git a/internal/grpc/services/gateway/appregistry.go b/internal/grpc/services/gateway/appregistry.go index a02f18f9de..cf50c14079 100644 --- a/internal/grpc/services/gateway/appregistry.go +++ b/internal/grpc/services/gateway/appregistry.go @@ -32,13 +32,30 @@ func (s *svc) GetAppProviders(ctx context.Context, req *registry.GetAppProviders if err != nil { err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient") return ®istry.GetAppProvidersResponse{ - Status: status.NewInternal(ctx, err, "error getting user share provider client"), + Status: status.NewInternal(ctx, err, "error getting app registry client"), }, nil } res, err := c.GetAppProviders(ctx, req) if err != nil { - return nil, errors.Wrap(err, "gateway: error calling ListShares") + return nil, errors.Wrap(err, "gateway: error calling GetAppProviders") + } + + return res, nil +} + +func (s *svc) AddAppProvider(ctx context.Context, req *registry.AddAppProviderRequest) (*registry.AddAppProviderResponse, error) { + c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient") + return ®istry.AddAppProviderResponse{ + Status: status.NewInternal(ctx, err, "error getting app registry client"), + }, nil + } + + res, err := c.AddAppProvider(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling AddAppProvider") } return res, nil @@ -49,13 +66,47 @@ func (s *svc) ListAppProviders(ctx context.Context, req *registry.ListAppProvide if err != nil { err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient") return ®istry.ListAppProvidersResponse{ - Status: status.NewInternal(ctx, err, "error getting user share provider client"), + Status: status.NewInternal(ctx, err, "error getting app registry client"), }, nil } res, err := c.ListAppProviders(ctx, req) if err != nil { - return nil, errors.Wrap(err, "gateway: error calling ListShares") + return nil, errors.Wrap(err, "gateway: error calling ListAppProviders") + } + + return res, nil +} + +func (s *svc) GetDefaultAppProviderForMimeType(ctx context.Context, req *registry.GetDefaultAppProviderForMimeTypeRequest) (*registry.GetDefaultAppProviderForMimeTypeResponse, error) { + c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient") + return ®istry.GetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewInternal(ctx, err, "error getting app registry client"), + }, nil + } + + res, err := c.GetDefaultAppProviderForMimeType(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetDefaultAppProviderForMimeType") + } + + return res, nil +} + +func (s *svc) SetDefaultAppProviderForMimeType(ctx context.Context, req *registry.SetDefaultAppProviderForMimeTypeRequest) (*registry.SetDefaultAppProviderForMimeTypeResponse, error) { + c, err := pool.GetAppRegistryClient(s.c.AppRegistryEndpoint) + if err != nil { + err = errors.Wrap(err, "gateway: error calling GetAppRegistryClient") + return ®istry.SetDefaultAppProviderForMimeTypeResponse{ + Status: status.NewInternal(ctx, err, "error getting app registry client"), + }, nil + } + + res, err := c.SetDefaultAppProviderForMimeType(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling SetDefaultAppProviderForMimeType") } return res, nil diff --git a/internal/http/services/appprovider/appprovider.go b/internal/http/services/appprovider/appprovider.go new file mode 100644 index 0000000000..bae42ca2c1 --- /dev/null +++ b/internal/http/services/appprovider/appprovider.go @@ -0,0 +1,232 @@ +// Copyright 2018-2021 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 ocmd + +import ( + "context" + "encoding/base64" + "encoding/json" + "net/http" + "net/url" + "strings" + "time" + "unicode/utf8" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/internal/http/services/ocmd" + "github.com/cs3org/reva/pkg/rgrpc/status" + "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/rhttp/global" + "github.com/cs3org/reva/pkg/sharedconf" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + "github.com/rs/zerolog" +) + +func init() { + global.Register("appprovider", New) +} + +// Config holds the config options that need to be passed down to all ocdav handlers +type Config struct { + Prefix string `mapstructure:"prefix"` + GatewaySvc string `mapstructure:"gatewaysvc"` + AccessTokenTTL int `mapstructure:"access_token_ttl"` +} + +func (c *Config) init() { + if c.Prefix == "" { + c.Prefix = "api/v0/wopi/open" + } + if c.AccessTokenTTL == 0 { + c.AccessTokenTTL = 86400 + } + c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) +} + +type svc struct { + conf *Config +} + +// New returns a new ocmd object +func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { + + conf := &Config{} + if err := mapstructure.Decode(m, conf); err != nil { + return nil, err + } + conf.init() + + s := &svc{ + conf: conf, + } + return s, nil +} + +// Close performs cleanup. +func (s *svc) Close() error { + return nil +} + +func (s *svc) Prefix() string { + return s.conf.Prefix +} + +func (s *svc) Unprotected() []string { + return []string{} +} + +func (s *svc) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + ocmd.WriteError(w, r, ocmd.APIErrorUnimplemented, "only GET requests are supported", errors.New("only GET requests are supported")) + return + } + + s.handleWopiOpen(w, r) + }) +} + +// WopiResponse holds the various fields to be returned for a wopi open call +type WopiResponse struct { + WopiClientURL string `json:"wopiclienturl"` + AccessToken string `json:"accesstoken"` + AccessTokenTTL int64 `json:"accesstokenttl"` +} + +func (s *svc) handleWopiOpen(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client, err := pool.GetGatewayServiceClient(s.conf.GatewaySvc) + if err != nil { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error getting grpc gateway client", err) + return + } + + info, errCode, err := s.getStatInfo(ctx, r.URL.Query().Get("fileId"), client) + if err != nil { + ocmd.WriteError(w, r, errCode, "error statting file", err) + } + + openReq := gateway.OpenInAppRequest{ + Ref: &provider.Reference{ResourceId: info.Id}, + ViewMode: getViewMode(info), + App: r.URL.Query().Get("app"), + } + openRes, err := client.OpenInApp(ctx, &openReq) + if err != nil { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error opening resource", err) + return + } + if openRes.Status.Code != rpc.Code_CODE_OK { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error opening resource information", status.NewErrorFromCode(openRes.Status.Code, "appprovider")) + return + } + + u, err := url.Parse(openRes.AppUrl) + if err != nil { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error parsing app URL", err) + return + } + q := u.Query() + + // remove access token from query parameters + accessToken := q.Get("access_token") + q.Del("access_token") + + // more options used by oC 10: + // &lang=en-GB + // &closebutton=1 + // &revisionhistory=1 + // &title=Hello.odt + u.RawQuery = q.Encode() + + js, err := json.Marshal( + WopiResponse{ + WopiClientURL: u.String(), + AccessToken: accessToken, + // https://wopi.readthedocs.io/projects/wopirest/en/latest/concepts.html#term-access-token-ttl + AccessTokenTTL: time.Now().Add(time.Second*time.Duration(s.conf.AccessTokenTTL)).UnixNano() / 1e6, + }, + ) + if err != nil { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error marshalling JSON response", err) + return + } + + w.Header().Set("Content-Type", "application/json") + if _, err = w.Write(js); err != nil { + ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error writing JSON response", err) + return + } +} + +func (s *svc) getStatInfo(ctx context.Context, fileID string, client gateway.GatewayAPIClient) (*provider.ResourceInfo, ocmd.APIErrorCode, error) { + if fileID == "" { + return nil, ocmd.APIErrorInvalidParameter, errors.New("fileID parameter missing in request") + } + + decodedID, err := base64.URLEncoding.DecodeString(fileID) + if err != nil { + return nil, ocmd.APIErrorInvalidParameter, errors.Wrap(err, "fileID doesn't follow the required format") + } + + parts := strings.Split(string(decodedID), ":") + if !utf8.ValidString(parts[0]) || !utf8.ValidString(parts[1]) { + return nil, ocmd.APIErrorInvalidParameter, errors.New("fileID contains illegal characters") + } + res := &provider.ResourceId{ + StorageId: parts[0], + OpaqueId: parts[1], + } + + statReq := provider.StatRequest{ + Ref: &provider.Reference{ResourceId: res}, + } + statRes, err := client.Stat(ctx, &statReq) + if err != nil { + return nil, ocmd.APIErrorServerError, err + } + if statRes.Status.Code != rpc.Code_CODE_OK { + return nil, ocmd.APIErrorServerError, status.NewErrorFromCode(statRes.Status.Code, "appprovider") + } + if statRes.Info.Type != provider.ResourceType_RESOURCE_TYPE_FILE { + return nil, ocmd.APIErrorServerError, errors.New("unsupported resource type") + } + + return statRes.Info, ocmd.APIErrorCode(""), nil +} + +func getViewMode(res *provider.ResourceInfo) gateway.OpenInAppRequest_ViewMode { + var viewMode gateway.OpenInAppRequest_ViewMode + canEdit := res.PermissionSet.InitiateFileUpload + canView := res.PermissionSet.InitiateFileDownload + + switch { + case canEdit && canView: + viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_WRITE + case canView: + viewMode = gateway.OpenInAppRequest_VIEW_MODE_READ_ONLY + default: + viewMode = gateway.OpenInAppRequest_VIEW_MODE_INVALID + } + return viewMode +} diff --git a/internal/http/services/ocmd/ocmd.go b/internal/http/services/ocmd/ocmd.go index 61def86fc9..da6ebe46ba 100644 --- a/internal/http/services/ocmd/ocmd.go +++ b/internal/http/services/ocmd/ocmd.go @@ -30,6 +30,10 @@ import ( "github.com/rs/zerolog" ) +func init() { + global.Register("ocmd", New) +} + // Config holds the config options that need to be passed down to all ocdav handlers type Config struct { SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"` @@ -56,10 +60,6 @@ type svc struct { InvitesHandler *invitesHandler } -func init() { - global.Register("ocmd", New) -} - // New returns a new ocmd object func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { diff --git a/pkg/app/app.go b/pkg/app/app.go index 1534094f2d..b4ff68925c 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -21,24 +21,24 @@ package app import ( "context" + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + registry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" ) // Registry is the interface that application registries implement // for discovering application providers type Registry interface { - FindProvider(ctx context.Context, mimeType string) (*ProviderInfo, error) - ListProviders(ctx context.Context) ([]*ProviderInfo, error) -} - -// ProviderInfo contains the information -// about a Application Provider -type ProviderInfo struct { - Location string + FindProviders(ctx context.Context, mimeType string) ([]*registry.ProviderInfo, error) + ListProviders(ctx context.Context) ([]*registry.ProviderInfo, error) + AddProvider(ctx context.Context, p *registry.ProviderInfo) error + GetDefaultProviderForMimeType(ctx context.Context, mimeType string) (*registry.ProviderInfo, error) + SetDefaultProviderForMimeType(ctx context.Context, mimeType string, p *registry.ProviderInfo) error } // Provider is the interface that application providers implement -// for providing the iframe location to a iframe UI Provider +// for providing the URL of the app which will serve the requested resource. type Provider interface { - GetIFrame(ctx context.Context, resID *provider.Reference, token string) (string, error) + GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token string) (string, 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 28134083e5..4805f2b1b9 100644 --- a/pkg/app/provider/demo/demo.go +++ b/pkg/app/provider/demo/demo.go @@ -24,19 +24,32 @@ import ( "github.com/cs3org/reva/pkg/app" - providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + appregistry "github.com/cs3org/go-cs3apis/cs3/app/registry/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/app/provider/registry" "github.com/mitchellh/mapstructure" ) -type provider struct { +func init() { + registry.Register("demo", New) +} + +type demoProvider struct { iframeUIProvider string } -func (p *provider) GetIFrame(ctx context.Context, resID *providerpb.Reference, token string) (string, error) { - msg := fmt.Sprintf("