From 4a81259a5cc31ce8e3b155632b3dc356841d12e4 Mon Sep 17 00:00:00 2001 From: "A.Unger" Date: Mon, 6 Sep 2021 12:08:50 +0200 Subject: [PATCH] spaces: WIP CreateStorageSoace spaces: refactor decomposedfs spaces related code into spaces.go spaces: move list spaces to the new spaces.go file spaces: move private create space to the new spaces.go file space: spaces can have duplicated names space: fix linter space: uint conversions linter: file header linter: change receiver name linter: space out comments spaces: refactor and cleanup a bit around uuids xattrs: add space name extended attribute, as opposed to node name lint: redundancy ... lint: ... xattrs: comment on space.name add changelog spaces: a space creator becomes an editor within the new resource escape ldap filters (#2042) fix the storageid of shares (#2033) [Build-deps]: Bump github.com/go-chi/chi/v5 from 5.0.3 to 5.0.4 (#2038) Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.0.3 to 5.0.4. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.0.3...v5.0.4) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [Build-deps]: Bump github.com/aws/aws-sdk-go from 1.40.17 to 1.40.37 (#2040) Bumps [github.com/aws/aws-sdk-go](https://github.com/aws/aws-sdk-go) from 1.40.17 to 1.40.37. - [Release notes](https://github.com/aws/aws-sdk-go/releases) - [Changelog](https://github.com/aws/aws-sdk-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-go/compare/v1.40.17...v1.40.37) --- updated-dependencies: - dependency-name: github.com/aws/aws-sdk-go dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> [Build-deps]: Bump go.opentelemetry.io/otel/sdk (#2039) Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.0.0-RC2 to 1.0.0-RC3. - [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.0.0-RC2...v1.0.0-RC3) --- updated-dependencies: - dependency-name: go.opentelemetry.io/otel/sdk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> runtime: do not truncate logs on reload (#2047) Bump core commit id (#2048) update cs3apis, add utility methods for share filters (#2044) Fail initialization if app is unsupported (#2034) Nextcloud storage integration tests & add Nextcloud drivers for auth and user (#2043) --- .drone.env | 2 +- .../unreleased/appprovider-unsupported.md | 5 + changelog/unreleased/escape-ldap-filter.md | 5 + changelog/unreleased/fix-revad-logging.md | 8 + changelog/unreleased/fix-storageid-shares.md | 5 + .../unreleased/nextcloud-user-backend.md | 6 + .../unreleased/sharing-filter-methods.md | 5 + changelog/unreleased/spaces-creation.md | 5 + cmd/reva/ocm-share-list.go | 10 +- cmd/reva/public-share-list.go | 10 +- cmd/reva/share-list.go | 10 +- cmd/revad/runtime/runtime.go | 2 +- examples/nextcloud-integration/revad.toml | 140 ++++++++ go.mod | 12 +- go.sum | 20 +- .../grpc/services/gateway/storageprovider.go | 2 +- .../storageprovider/storageprovider.go | 5 +- .../usershareprovider/usershareprovider.go | 2 +- .../handlers/apps/sharing/shares/shares.go | 28 +- .../ocs/handlers/apps/sharing/shares/user.go | 2 +- pkg/app/provider/wopi/wopi.go | 14 +- pkg/auth/manager/ldap/ldap.go | 2 +- pkg/auth/manager/loader/loader.go | 1 + pkg/auth/manager/nextcloud/nextcloud.go | 152 +++++++++ pkg/cbox/share/sql/sql.go | 6 +- pkg/group/manager/ldap/ldap.go | 6 +- pkg/ocm/share/share.go | 10 + pkg/publicshare/publicshare.go | 10 + pkg/share/manager/json/json.go | 6 +- pkg/share/manager/memory/memory.go | 6 +- pkg/share/manager/sql/sql.go | 6 +- pkg/share/manager/sql/sql_test.go | 23 +- pkg/share/share.go | 34 +- pkg/storage/fs/nextcloud/nextcloud.go | 193 ++++++++--- .../fs/nextcloud/nextcloud_server_mock.go | 131 ++++--- pkg/storage/fs/nextcloud/nextcloud_test.go | 38 ++- pkg/storage/fs/owncloud/owncloud.go | 5 + pkg/storage/fs/owncloud/owncloud_unix.go | 1 + pkg/storage/fs/owncloud/owncloud_windows.go | 1 + pkg/storage/fs/owncloudsql/owncloudsql.go | 5 + .../fs/owncloudsql/owncloudsql_unix.go | 1 + pkg/storage/fs/s3/s3.go | 5 + pkg/storage/storage.go | 1 + .../utils/decomposedfs/decomposedfs.go | 176 ---------- .../utils/decomposedfs/decomposedfs_unix.go | 1 + pkg/storage/utils/decomposedfs/grants.go | 11 +- pkg/storage/utils/decomposedfs/node/node.go | 10 + pkg/storage/utils/decomposedfs/spaces.go | 323 ++++++++++++++++++ .../utils/decomposedfs/xattrs/xattrs.go | 6 +- pkg/storage/utils/eosfs/eosfs.go | 5 + pkg/storage/utils/localfs/localfs.go | 5 + pkg/storage/utils/localfs/localfs_unix.go | 1 + pkg/user/manager/ldap/ldap.go | 6 +- pkg/user/manager/loader/loader.go | 1 + pkg/user/manager/nextcloud/nextcloud.go | 155 +++++++++ pkg/user/manager/registry/registry.go | 7 +- .../expected-failures-on-OCIS-storage.md | 1 - .../expected-failures-on-S3NG-storage.md | 1 - tests/helpers/helpers.go | 19 -- .../fixtures/storageprovider-nextcloud.toml | 2 +- .../integration/grpc/storageprovider_test.go | 3 +- 61 files changed, 1268 insertions(+), 406 deletions(-) create mode 100644 changelog/unreleased/appprovider-unsupported.md create mode 100644 changelog/unreleased/escape-ldap-filter.md create mode 100644 changelog/unreleased/fix-revad-logging.md create mode 100644 changelog/unreleased/fix-storageid-shares.md create mode 100644 changelog/unreleased/nextcloud-user-backend.md create mode 100644 changelog/unreleased/sharing-filter-methods.md create mode 100644 changelog/unreleased/spaces-creation.md create mode 100644 examples/nextcloud-integration/revad.toml create mode 100644 pkg/auth/manager/nextcloud/nextcloud.go create mode 100644 pkg/storage/utils/decomposedfs/spaces.go create mode 100644 pkg/user/manager/nextcloud/nextcloud.go diff --git a/.drone.env b/.drone.env index f078382aa2..6558db87ad 100644 --- a/.drone.env +++ b/.drone.env @@ -1,3 +1,3 @@ # The test runner source for API tests -CORE_COMMITID=901157f2b3d0ba1f36b1e4d22c8384e255094b11 +CORE_COMMITID=370fd807e464dcc5cb9619a8890107424254dfa6 CORE_BRANCH=master diff --git a/changelog/unreleased/appprovider-unsupported.md b/changelog/unreleased/appprovider-unsupported.md new file mode 100644 index 0000000000..271ccb4fa9 --- /dev/null +++ b/changelog/unreleased/appprovider-unsupported.md @@ -0,0 +1,5 @@ +Enhancement: fail initialization of a WOPI AppProvider if +the underlying app is not WOPI-compliant nor it is supported +by the WOPI bridge extensions + +https://github.com/cs3org/reva/pull/2034 diff --git a/changelog/unreleased/escape-ldap-filter.md b/changelog/unreleased/escape-ldap-filter.md new file mode 100644 index 0000000000..a1bad3ecdf --- /dev/null +++ b/changelog/unreleased/escape-ldap-filter.md @@ -0,0 +1,5 @@ +Enhancement: escape ldap filters + +Added ldap filter escaping to increase the security of reva. + +https://github.com/cs3org/reva/pull/2042 diff --git a/changelog/unreleased/fix-revad-logging.md b/changelog/unreleased/fix-revad-logging.md new file mode 100644 index 0000000000..fa397da9f3 --- /dev/null +++ b/changelog/unreleased/fix-revad-logging.md @@ -0,0 +1,8 @@ +Bugfix: Do not truncate logs on restart + +This change fixes the way log files were opened. +Before they were truncated and now the log file +will be open in append mode and created it if it +does not exist. + +https://github.com/cs3org/reva/pull/2047 diff --git a/changelog/unreleased/fix-storageid-shares.md b/changelog/unreleased/fix-storageid-shares.md new file mode 100644 index 0000000000..077cd32bb6 --- /dev/null +++ b/changelog/unreleased/fix-storageid-shares.md @@ -0,0 +1,5 @@ +Bugfix: Fix the storage id of shares + +The storageid in the share object contained an incorrect value. + +https://github.com/cs3org/reva/pull/2033 diff --git a/changelog/unreleased/nextcloud-user-backend.md b/changelog/unreleased/nextcloud-user-backend.md new file mode 100644 index 0000000000..190367be77 --- /dev/null +++ b/changelog/unreleased/nextcloud-user-backend.md @@ -0,0 +1,6 @@ +Enhancement: Nextcloud user backend + +Adds Nextcloud as a user backend (Nextcloud drivers for 'auth' and 'user'). +Also adds back the Nextcloud storage integration tests. + +https://github.com/cs3org/reva/pull/2043 \ No newline at end of file diff --git a/changelog/unreleased/sharing-filter-methods.md b/changelog/unreleased/sharing-filter-methods.md new file mode 100644 index 0000000000..7b90d29d74 --- /dev/null +++ b/changelog/unreleased/sharing-filter-methods.md @@ -0,0 +1,5 @@ +Enhancement: Add utility methods for creating share filters + +Updated the CS3 API to include the new share grantee filter and added utility methods for creating share filters. This will help making the code more concise. + +https://github.com/cs3org/reva/pull/2044 diff --git a/changelog/unreleased/spaces-creation.md b/changelog/unreleased/spaces-creation.md new file mode 100644 index 0000000000..36221bc280 --- /dev/null +++ b/changelog/unreleased/spaces-creation.md @@ -0,0 +1,5 @@ +Enhancement: Create operations for Spaces + +DecomposedFS is aware now of the concept of Spaces, and supports for creating them. + +https://github.com/cs3org/reva/pull/2041 diff --git a/cmd/reva/ocm-share-list.go b/cmd/reva/ocm-share-list.go index 1bff9bcd3b..ed246a8c26 100644 --- a/cmd/reva/ocm-share-list.go +++ b/cmd/reva/ocm-share-list.go @@ -29,6 +29,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/ocm/share" "github.com/jedib0t/go-pretty/table" ) @@ -60,14 +61,7 @@ func ocmShareListCommand() *command { StorageId: tokens[0], OpaqueId: tokens[1], } - shareRequest.Filters = []*ocm.ListOCMSharesRequest_Filter{ - { - Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{ - ResourceId: id, - }, - }, - } + shareRequest.Filters = []*ocm.ListOCMSharesRequest_Filter{share.ResourceIDFilter(id)} } shareRes, err := shareClient.ListOCMShares(ctx, shareRequest) diff --git a/cmd/reva/public-share-list.go b/cmd/reva/public-share-list.go index 1a0a2f1a74..baa13e4ad5 100644 --- a/cmd/reva/public-share-list.go +++ b/cmd/reva/public-share-list.go @@ -29,6 +29,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/publicshare" "github.com/jedib0t/go-pretty/table" ) @@ -60,14 +61,7 @@ func publicShareListCommand() *command { StorageId: tokens[0], OpaqueId: tokens[1], } - shareRequest.Filters = []*link.ListPublicSharesRequest_Filter{ - { - Type: link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &link.ListPublicSharesRequest_Filter_ResourceId{ - ResourceId: id, - }, - }, - } + shareRequest.Filters = []*link.ListPublicSharesRequest_Filter{publicshare.ResourceIDFilter(id)} } shareRes, err := shareClient.ListPublicShares(ctx, shareRequest) diff --git a/cmd/reva/share-list.go b/cmd/reva/share-list.go index 9ec94a5999..fa00b1dc0b 100644 --- a/cmd/reva/share-list.go +++ b/cmd/reva/share-list.go @@ -29,6 +29,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/pkg/share" "github.com/jedib0t/go-pretty/table" ) @@ -60,14 +61,7 @@ func shareListCommand() *command { StorageId: tokens[0], OpaqueId: tokens[1], } - shareRequest.Filters = []*collaboration.ListSharesRequest_Filter{ - { - Type: collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &collaboration.ListSharesRequest_Filter_ResourceId{ - ResourceId: id, - }, - }, - } + shareRequest.Filters = []*collaboration.Filter{share.ResourceIDFilter(id)} } shareRes, err := shareClient.ListShares(ctx, shareRequest) diff --git a/cmd/revad/runtime/runtime.go b/cmd/revad/runtime/runtime.go index 458caf401d..22d55eb30b 100644 --- a/cmd/revad/runtime/runtime.go +++ b/cmd/revad/runtime/runtime.go @@ -235,7 +235,7 @@ func getWriter(out string) (io.Writer, error) { return os.Stdout, nil } - fd, err := os.Create(out) + fd, err := os.OpenFile(out, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { err = errors.Wrap(err, "error creating log file: "+out) return nil, err diff --git a/examples/nextcloud-integration/revad.toml b/examples/nextcloud-integration/revad.toml new file mode 100644 index 0000000000..6c9b0c0615 --- /dev/null +++ b/examples/nextcloud-integration/revad.toml @@ -0,0 +1,140 @@ +[shared] +gatewaysvc = "localhost:19000" + +[grpc] +address = "0.0.0.0:19000" + +[grpc.services.gateway] +authregistrysvc = "localhost:19000" +appprovidersvc = "localhost:19000" +appregistry = "localhost:19000" +storageregistrysvc = "localhost:19000" +preferencessvc = "localhost:19000" +userprovidersvc = "localhost:19000" +usershareprovidersvc = "localhost:19000" +publicshareprovidersvc = "localhost:19000" +ocmcoresvc = "localhost:19000" +ocmshareprovidersvc = "localhost:19000" +ocminvitemanagersvc = "localhost:19000" +ocmproviderauthorizersvc = "localhost:19000" +commit_share_to_storage_grant = false +datagateway = "http://localhost:19001/data" +transfer_expires = 6 # give it a moment + +[grpc.services.authregistry] +driver = "static" + +[grpc.services.authregistry.drivers.static.rules] +basic = "localhost:19000" + +[grpc.services.storageregistry] +driver = "static" + +[grpc.services.storageregistry.drivers.static] +home_provider = "/home" + +[grpc.services.storageregistry.drivers.static.rules] +"/home" = {"address" = "localhost:19000"} +"123e4567-e89b-12d3-a456-426655440000" = {"address" = "localhost:19000"} + +[grpc.services.usershareprovider] +driver = "memory" + +[grpc.services.ocmcore] +driver = "json" + +# Note that ocmcore and ocmshareprovider should use the same file for storing the shares. +[grpc.services.ocmcore.drivers.json] +file = "/var/tmp/reva/shares_server_1.json" + +[grpc.services.ocminvitemanager] +driver = "json" + +[grpc.services.ocmshareprovider] +driver = "json" + +[grpc.services.ocmshareprovider.drivers.json] +file = "/var/tmp/reva/shares_server_1.json" + +[grpc.services.ocmproviderauthorizer] +driver = "json" + +[grpc.services.ocmproviderauthorizer.drivers.json] +providers = "/etc/revad/providers.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.storageprovider] +driver = "nextcloud" +mount_path = "/home" +mount_id = "123e4567-e89b-12d3-a456-426655440000" +expose_data_server = true +data_server_url = "http://127.0.0.1:19001/data" +enable_home_creation = true +disable_tus = true + +[grpc.services.storageprovider.mimetypes] +".zmd" = "application/compressed-markdown" + +[grpc.services.storageprovider.drivers.nextcloud] +end_point = "http://localhost/apps/sciencemesh/" +user_layout = "{{.Username}}" + + +[grpc.services.authprovider] +auth_manager = "nextcloud" +[grpc.services.authprovider.drivers.nextcloud] +end_point = "http://localhost/apps/sciencemesh/" + +[grpc.services.userprovider] +driver = "nextcloud" +[grpc.services.userprovider.drivers.nextcloud] +end_point = "http://localhost/apps/sciencemesh/" + +[http] +enabled_services = ["ocmd"] +enabled_middlewares = ["providerauthorizer", "cors"] +address = "0.0.0.0:19001" + +[http.services.dataprovider] +driver = "nextcloud" + +[http.services.prometheus] +[http.services.sysinfo] + +[http.services.dataprovider.drivers.localhome] +user_layout = "{{.Username}}" + +[http.services.ocmd] +prefix = "ocm" + +[http.middlewares.providerauthorizer] +driver = "json" + +[http.middlewares.providerauthorizer.drivers.json] +providers = "/etc/revad/providers.json" + +[http.services.ocs] +prefix = "ocs" + +[http.services.ocdav] + +[http.middlewares.cors] diff --git a/go.mod b/go.mod index cfad917fbe..25949f9a7d 100644 --- a/go.mod +++ b/go.mod @@ -9,18 +9,18 @@ require ( github.com/Masterminds/sprig v2.22.0+incompatible 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.17 + github.com/aws/aws-sdk-go v1.40.37 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 github.com/coreos/go-oidc v2.2.1+incompatible github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20210812121411-f18cf19614e8 + github.com/cs3org/go-cs3apis v0.0.0-20210906133842-03e4a408c1f3 github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 github.com/gdexlab/go-render v1.0.1 - github.com/go-chi/chi/v5 v5.0.3 + github.com/go-chi/chi/v5 v5.0.4 github.com/go-ldap/ldap/v3 v3.3.0 github.com/go-openapi/errors v0.20.0 // indirect github.com/go-openapi/strfmt v0.19.5 // indirect @@ -61,10 +61,10 @@ require ( go.mongodb.org/mongo-driver v1.5.1 // indirect go.opencensus.io v0.23.0 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.22.0 - go.opentelemetry.io/otel v1.0.0-RC2 + go.opentelemetry.io/otel v1.0.0-RC3 go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC2 - go.opentelemetry.io/otel/sdk v1.0.0-RC2 - go.opentelemetry.io/otel/trace v1.0.0-RC2 + go.opentelemetry.io/otel/sdk v1.0.0-RC3 + go.opentelemetry.io/otel/trace v1.0.0-RC3 golang.org/x/crypto v0.0.0-20201216223049-8b5274cf687f golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 diff --git a/go.sum b/go.sum index 69435978f2..2dff948d97 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,8 @@ github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0 github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/aws/aws-sdk-go v1.20.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= -github.com/aws/aws-sdk-go v1.40.17 h1:WcE72YOL7ChzAWlgpEv9YMOqAwJDM1yzkv4GxWyS5wk= -github.com/aws/aws-sdk-go v1.40.17/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= +github.com/aws/aws-sdk-go v1.40.37 h1:I+Q6cLctkFyMMrKukcDnj+i2kjrQ37LGiOM6xmsxC48= +github.com/aws/aws-sdk-go v1.40.37/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q= 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= @@ -92,6 +92,8 @@ github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJff github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= github.com/cs3org/go-cs3apis v0.0.0-20210812121411-f18cf19614e8 h1:bqUkE0l5wD62TKH6HkbSVYwyYzZ0PIeak/hnW7ggUdU= github.com/cs3org/go-cs3apis v0.0.0-20210812121411-f18cf19614e8/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20210906133842-03e4a408c1f3 h1:NcLk09WK4wx/iIrEI+7ZFbr78APaRKJxF0+zl6kv4is= +github.com/cs3org/go-cs3apis v0.0.0-20210906133842-03e4a408c1f3/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -113,8 +115,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.5.0/go.mod h1:Nd6IXA8m5kNZdNEHMBd93KT+mdY3+bewLgRvmCsR2Do= github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8= github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= -github.com/go-chi/chi/v5 v5.0.3 h1:khYQBdPivkYG1s1TAzDQG1f6eX4kD2TItYVZexL5rS4= -github.com/go-chi/chi/v5 v5.0.3/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-chi/chi/v5 v5.0.4 h1:5e494iHzsYBiyXQAHHuI4tyJS9M3V84OuX3ufIIGHFo= +github.com/go-chi/chi/v5 v5.0.4/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -326,7 +328,6 @@ github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mileusna/useragent v1.0.2 h1:DgVKtiPnjxlb73z9bCwgdUvU2nQNQ97uhgfO8l9uz/w= github.com/mileusna/useragent v1.0.2/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc= github.com/minio/md5-simd v1.1.0 h1:QPfiOqlZH+Cj9teu0t9b1nTBfPbyTl16Of5MeuShdK4= @@ -485,16 +486,19 @@ go.opentelemetry.io/contrib v0.22.0 h1:0F7gDEjgb1WGn4ODIjaCAg75hmqF+UN0LiVgwxsCo go.opentelemetry.io/contrib v0.22.0/go.mod h1:EH4yDYeNoaTqn/8yCWQmfNB78VHfGX2Jt2bvnvzBlGM= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.22.0 h1:TjqELdtCtlOJQrTnXd2y+RP6wXKZUnnJer0HR0CSo18= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.22.0/go.mod h1:KjqwX4uJNaj479ZjFpADOMJKOM4rBXq4kN7nbeuGKrY= -go.opentelemetry.io/otel v1.0.0-RC2 h1:SHhxSjB+omnGZPgGlKe+QMp3MyazcOHdQ8qwo89oKbg= go.opentelemetry.io/otel v1.0.0-RC2/go.mod h1:w1thVQ7qbAy8MHb0IFj8a5Q2QU0l2ksf8u/CN8m3NOM= +go.opentelemetry.io/otel v1.0.0-RC3 h1:kvwiyEkiUT/JaadXzVLI/R1wDO934A7r3Bs2wEe6wqA= +go.opentelemetry.io/otel v1.0.0-RC3/go.mod h1:Ka5j3ua8tZs4Rkq4Ex3hwgBgOchyPVq5S6P2lz//nKQ= go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC2 h1:RF0nWsIDpDBe+s06lkLxUw9CWQUAhO6hBSxxB7dz45s= go.opentelemetry.io/otel/exporters/jaeger v1.0.0-RC2/go.mod h1:sZZqN3Vb0iT+NE6mZ1S7sNyH3t4PFk6ElK5TLGFBZ7E= go.opentelemetry.io/otel/oteltest v1.0.0-RC2 h1:xNKqMhlZYkASSyvF4JwObZFMq0jhFN3c3SP+2rCzVPk= go.opentelemetry.io/otel/oteltest v1.0.0-RC2/go.mod h1:kiQ4tw5tAL4JLTbcOYwK1CWI1HkT5aiLzHovgOVnz/A= -go.opentelemetry.io/otel/sdk v1.0.0-RC2 h1:ROuteeSCBaZNjiT9JcFzZepmInDvLktR28Y6qKo8bCs= go.opentelemetry.io/otel/sdk v1.0.0-RC2/go.mod h1:fgwHyiDn4e5k40TD9VX243rOxXR+jzsWBZYA2P5jpEw= -go.opentelemetry.io/otel/trace v1.0.0-RC2 h1:dunAP0qDULMIT82atj34m5RgvsIK6LcsXf1c/MsYg1w= +go.opentelemetry.io/otel/sdk v1.0.0-RC3 h1:iRMkET+EmJUn5mW0hJzygBraXRmrUwzbOtNvTCh/oKs= +go.opentelemetry.io/otel/sdk v1.0.0-RC3/go.mod h1:78H6hyg2fka0NYT9fqGuFLvly2yCxiBXDJAgLKo/2Us= go.opentelemetry.io/otel/trace v1.0.0-RC2/go.mod h1:JPQ+z6nNw9mqEGT8o3eoPTdnNI+Aj5JcxEsVGREIAy4= +go.opentelemetry.io/otel/trace v1.0.0-RC3 h1:9F0ayEvlxv8BmNmPbU005WK7hC+7KbOazCPZjNa1yME= +go.opentelemetry.io/otel/trace v1.0.0-RC3/go.mod h1:VUt2TUYd8S2/ZRX09ZDFZQwn2RqfMB5MzO17jBojGxo= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 5dd46af077..3aeb4eccf9 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -101,7 +101,7 @@ func (s *svc) CreateHome(ctx context.Context, req *provider.CreateHomeRequest) ( func (s *svc) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { log := appctx.GetLogger(ctx) // TODO: needs to be fixed - c, err := s.findByPath(ctx, req.Type) + c, err := s.findByPath(ctx, "/users") if err != nil { return &provider.CreateStorageSpaceResponse{ Status: status.NewStatusFromErrType(ctx, "error finding path", err), diff --git a/internal/grpc/services/storageprovider/storageprovider.go b/internal/grpc/services/storageprovider/storageprovider.go index 9b681d40a9..c6e6d7f060 100644 --- a/internal/grpc/services/storageprovider/storageprovider.go +++ b/internal/grpc/services/storageprovider/storageprovider.go @@ -431,10 +431,9 @@ func (s *service) CreateHome(ctx context.Context, req *provider.CreateHomeReques return res, nil } +// CreateStorageSpace creates a storage space func (s *service) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { - return &provider.CreateStorageSpaceResponse{ - Status: status.NewUnimplemented(ctx, errtypes.NotSupported("CreateStorageSpace not implemented"), "CreateStorageSpace not implemented"), - }, nil + return s.storage.CreateStorageSpace(ctx, req) } func hasNodeID(s *provider.StorageSpace) bool { diff --git a/internal/grpc/services/usershareprovider/usershareprovider.go b/internal/grpc/services/usershareprovider/usershareprovider.go index 4130dacc77..4a582a40c6 100644 --- a/internal/grpc/services/usershareprovider/usershareprovider.go +++ b/internal/grpc/services/usershareprovider/usershareprovider.go @@ -187,7 +187,7 @@ func (s *service) UpdateShare(ctx context.Context, req *collaboration.UpdateShar } func (s *service) ListReceivedShares(ctx context.Context, req *collaboration.ListReceivedSharesRequest) (*collaboration.ListReceivedSharesResponse, error) { - shares, err := s.sm.ListReceivedShares(ctx) // TODO(labkode): check what to update + shares, err := s.sm.ListReceivedShares(ctx, req.Filters) // TODO(labkode): check what to update if err != nil { return &collaboration.ListReceivedSharesResponse{ Status: status.NewInternal(ctx, err, "error listing received shares"), diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go index 31409e512d..dd5ca8c682 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/shares.go @@ -49,13 +49,19 @@ import ( "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/internal/http/services/owncloud/ocs/response" "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/publicshare" "github.com/cs3org/reva/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/pkg/share" "github.com/cs3org/reva/pkg/share/cache" "github.com/cs3org/reva/pkg/share/cache/registry" "github.com/cs3org/reva/pkg/utils" "github.com/pkg/errors" ) +const ( + storageIDPrefix string = "shared::" +) + // Handler implements the shares part of the ownCloud sharing API type Handler struct { gatewayAddr string @@ -607,7 +613,7 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) { func (h *Handler) listSharesWithOthers(w http.ResponseWriter, r *http.Request) { shares := make([]*conversions.ShareData, 0) - filters := []*collaboration.ListSharesRequest_Filter{} + filters := []*collaboration.Filter{} linkFilters := []*link.ListPublicSharesRequest_Filter{} var e error @@ -663,8 +669,8 @@ func (h *Handler) logProblems(s *rpc.Status, e error, msg string) { } } -func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix string) ([]*collaboration.ListSharesRequest_Filter, []*link.ListPublicSharesRequest_Filter, error) { - collaborationFilters := []*collaboration.ListSharesRequest_Filter{} +func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix string) ([]*collaboration.Filter, []*link.ListPublicSharesRequest_Filter, error) { + collaborationFilters := []*collaboration.Filter{} linkFilters := []*link.ListPublicSharesRequest_Filter{} ctx := r.Context() @@ -692,19 +698,9 @@ func (h *Handler) addFilters(w http.ResponseWriter, r *http.Request, prefix stri return nil, nil, err } - collaborationFilters = append(collaborationFilters, &collaboration.ListSharesRequest_Filter{ - Type: collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &collaboration.ListSharesRequest_Filter_ResourceId{ - ResourceId: info.Id, - }, - }) + collaborationFilters = append(collaborationFilters, share.ResourceIDFilter(info.Id)) - linkFilters = append(linkFilters, &link.ListPublicSharesRequest_Filter{ - Type: link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &link.ListPublicSharesRequest_Filter_ResourceId{ - ResourceId: info.Id, - }, - }) + linkFilters = append(linkFilters, publicshare.ResourceIDFilter(info.Id)) return collaborationFilters, linkFilters, nil } @@ -732,7 +728,6 @@ func (h *Handler) addFileInfo(ctx context.Context, s *conversions.ShareData, inf } s.MimeType = parsedMt // TODO STime: &types.Timestamp{Seconds: info.Mtime.Seconds, Nanos: info.Mtime.Nanos}, - s.StorageID = info.Id.StorageId + "!" + info.Id.OpaqueId // TODO Storage: int s.ItemSource = wrapResourceID(info.Id) s.FileSource = s.ItemSource @@ -742,6 +737,7 @@ func (h *Handler) addFileInfo(ctx context.Context, s *conversions.ShareData, inf s.FileTarget = path.Join(h.sharePrefix, path.Base(info.Path)) } s.Path = path.Join("/", path.Base(info.Path)) // TODO hm this might have to be relative to the users home ... depends on the webdav_namespace config + s.StorageID = storageIDPrefix + s.FileTarget // TODO FileParent: // item type s.ItemType = conversions.ResourceType(info.GetType()).String() diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go index 1a133f34b6..7ce8822d97 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/user.go @@ -120,7 +120,7 @@ func (h *Handler) removeUserShare(w http.ResponseWriter, r *http.Request, shareI response.WriteOCSSuccess(w, r, nil) } -func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.ListSharesRequest_Filter) ([]*conversions.ShareData, *rpc.Status, error) { +func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filter) ([]*conversions.ShareData, *rpc.Status, error) { ctx := r.Context() log := appctx.GetLogger(ctx) diff --git a/pkg/app/provider/wopi/wopi.go b/pkg/app/provider/wopi/wopi.go index 0f731d76af..1c7600c940 100644 --- a/pkg/app/provider/wopi/wopi.go +++ b/pkg/app/provider/wopi/wopi.go @@ -136,9 +136,6 @@ func (p *wopiProvider) GetAppURL(ctx context.Context, resource *provider.Resourc q.Add("fileid", resource.GetId().OpaqueId) q.Add("endpoint", resource.GetId().StorageId) q.Add("viewmode", 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", "...") u, ok := ctxpkg.ContextGetUser(ctx) if ok { // else defaults to "Anonymous Guest" q.Add("username", u.Username) @@ -287,17 +284,18 @@ func getAppURLs(c *config) (map[string]map[string]string, error) { // scrape app's home page to find the appname if !strings.Contains(buf.String(), c.AppName) { - // || (c.AppName != "CodiMD" && c.AppName != "Etherpad") { return nil, errors.New("Application server at " + c.AppURL + " does not match this AppProvider for " + c.AppName) } // register the supported mimetypes in the AppRegistry: this is hardcoded for the time being - if c.AppName == "CodiMD" { + switch c.AppName { + case "CodiMD": appURLs = getCodimdExtensions(c.AppURL) - } else if c.AppName == "Etherpad" { + case "Etherpad": appURLs = getEtherpadExtensions(c.AppURL) + default: + return nil, errors.New("Application server " + c.AppName + " running at " + c.AppURL + " is unsupported") } - } return appURLs, nil } @@ -370,7 +368,7 @@ func getCodimdExtensions(appURL string) map[string]map[string]string { func getEtherpadExtensions(appURL string) map[string]map[string]string { appURLs := make(map[string]map[string]string) appURLs["edit"] = map[string]string{ - ".etherpad": appURL, + ".epd": appURL, } return appURLs } diff --git a/pkg/auth/manager/ldap/ldap.go b/pkg/auth/manager/ldap/ldap.go index cefd1adef0..b136a05f5d 100644 --- a/pkg/auth/manager/ldap/ldap.go +++ b/pkg/auth/manager/ldap/ldap.go @@ -244,5 +244,5 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) } func (am *mgr) getLoginFilter(login string) string { - return strings.ReplaceAll(am.c.LoginFilter, "{{login}}", login) + return strings.ReplaceAll(am.c.LoginFilter, "{{login}}", ldap.EscapeFilter(login)) } diff --git a/pkg/auth/manager/loader/loader.go b/pkg/auth/manager/loader/loader.go index 1c6c7eff15..f2a756cb0c 100644 --- a/pkg/auth/manager/loader/loader.go +++ b/pkg/auth/manager/loader/loader.go @@ -26,6 +26,7 @@ import ( _ "github.com/cs3org/reva/pkg/auth/manager/json" _ "github.com/cs3org/reva/pkg/auth/manager/ldap" _ "github.com/cs3org/reva/pkg/auth/manager/machine" + _ "github.com/cs3org/reva/pkg/auth/manager/nextcloud" _ "github.com/cs3org/reva/pkg/auth/manager/oidc" _ "github.com/cs3org/reva/pkg/auth/manager/publicshares" // Add your own here diff --git a/pkg/auth/manager/nextcloud/nextcloud.go b/pkg/auth/manager/nextcloud/nextcloud.go new file mode 100644 index 0000000000..98889346c5 --- /dev/null +++ b/pkg/auth/manager/nextcloud/nextcloud.go @@ -0,0 +1,152 @@ +// 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 nextcloud verifies a clientID and clientSecret against a Nextcloud backend. +package nextcloud + +import ( + "context" + "encoding/json" + "io" + "net/http" + "strings" + + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + "github.com/cs3org/reva/pkg/auth" + "github.com/cs3org/reva/pkg/auth/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" +) + +func init() { + registry.Register("nextcloud", New) +} + +type mgr struct { + client *http.Client + endPoint string +} + +type config struct { + EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"` +} + +// Action describes a REST request to forward to the Nextcloud backend +type Action struct { + verb string + username string + argS string +} + +func (c *config) init() { +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + return c, nil +} + +// New returns an auth manager implementation that verifies against a Nextcloud backend. +func New(m map[string]interface{}) (auth.Manager, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + c.init() + + return &mgr{ + endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" + client: &http.Client{}, + }, nil +} + +func (am *mgr) Configure(ml map[string]interface{}) error { + return nil +} + +func (am *mgr) do(ctx context.Context, a Action) (int, []byte, error) { + log := appctx.GetLogger(ctx) + // user, err := getUser(ctx) + // if err != nil { + // return 0, nil, err + // } + // url := am.endPoint + "~" + a.username + "/api/" + a.verb + url := "http://localhost/apps/sciencemesh/~" + a.username + "/api/" + a.verb + log.Info().Msgf("am.do %s %s", url, a.argS) + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) + if err != nil { + return 0, nil, err + } + + req.Header.Set("Content-Type", "application/json") + resp, err := am.client.Do(req) + if err != nil { + return 0, nil, err + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return 0, nil, err + } + + log.Info().Msgf("am.do response %d %s", resp.StatusCode, body) + return resp.StatusCode, body, nil +} + +// Authenticate method as defined in https://github.com/cs3org/reva/blob/28500a8/pkg/auth/auth.go#L31-L33 +func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) { + var params = map[string]string{ + "password": clientSecret, + // "username": clientID, + } + bodyStr, err := json.Marshal(params) + if err != nil { + return nil, nil, err + } + log := appctx.GetLogger(ctx) + log.Info().Msgf("Authenticate %s %s", clientID, bodyStr) + + statusCode, _, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)}) + + if err != nil { + return nil, nil, err + } + + if statusCode != 200 { + return nil, nil, errors.New("Username/password not recognized by Nextcloud backend") + } + user := &user.User{ + Username: clientID, + Id: &user.UserId{ + OpaqueId: clientID, + Idp: "localhost", + Type: 1, + }, + Mail: clientID, + DisplayName: clientID, + Groups: nil, + } + return user, nil, nil +} diff --git a/pkg/cbox/share/sql/sql.go b/pkg/cbox/share/sql/sql.go index ffe2c05e95..05c4a7c559 100644 --- a/pkg/cbox/share/sql/sql.go +++ b/pkg/cbox/share/sql/sql.go @@ -274,13 +274,13 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference return m.GetShare(ctx, ref) } -func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListSharesRequest_Filter) ([]*collaboration.Share, error) { +func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { uid := conversions.FormatUserID(ctxpkg.ContextMustGetUser(ctx).Id) query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE (orphan = 0 or orphan IS NULL) AND (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)" var filterQuery string params := []interface{}{uid, uid, 0, 1} for i, f := range filters { - if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { filterQuery += "(fileid_prefix=? AND item_source=?)" if i != len(filters)-1 { filterQuery += " AND " @@ -314,7 +314,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListShare } // we list the shares that are targeted to the user in context or to the user groups. -func (m *mgr) ListReceivedShares(ctx context.Context) ([]*collaboration.ReceivedShare, error) { +func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { user := ctxpkg.ContextMustGetUser(ctx) uid := conversions.FormatUserID(user.Id) diff --git a/pkg/group/manager/ldap/ldap.go b/pkg/group/manager/ldap/ldap.go index a41cec3625..196d608ade 100644 --- a/pkg/group/manager/ldap/ldap.go +++ b/pkg/group/manager/ldap/ldap.go @@ -393,10 +393,10 @@ func (m *manager) getMemberFilter(gid *grouppb.GroupId) string { } func (m *manager) getAttributeFilter(attribute, value string) string { - attr := strings.ReplaceAll(m.c.AttributeFilter, "{{attr}}", attribute) - return strings.ReplaceAll(attr, "{{value}}", value) + attr := strings.ReplaceAll(m.c.AttributeFilter, "{{attr}}", ldap.EscapeFilter(attribute)) + return strings.ReplaceAll(attr, "{{value}}", ldap.EscapeFilter(value)) } func (m *manager) getFindFilter(query string) string { - return strings.ReplaceAll(m.c.FindFilter, "{{query}}", query) + return strings.ReplaceAll(m.c.FindFilter, "{{query}}", ldap.EscapeFilter(query)) } diff --git a/pkg/ocm/share/share.go b/pkg/ocm/share/share.go index 9eb439a6e0..e4b7d1188d 100644 --- a/pkg/ocm/share/share.go +++ b/pkg/ocm/share/share.go @@ -55,3 +55,13 @@ type Manager interface { // UpdateReceivedShare updates the received share with share state. UpdateReceivedShare(ctx context.Context, ref *ocm.ShareReference, f *ocm.UpdateReceivedOCMShareRequest_UpdateField) (*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{ + Type: ocm.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID, + Term: &ocm.ListOCMSharesRequest_Filter_ResourceId{ + ResourceId: id, + }, + } +} diff --git a/pkg/publicshare/publicshare.go b/pkg/publicshare/publicshare.go index 0cab57363a..fa00beb32c 100644 --- a/pkg/publicshare/publicshare.go +++ b/pkg/publicshare/publicshare.go @@ -83,3 +83,13 @@ func AddSignature(share *link.PublicShare, pw string) error { } return nil } + +// ResourceIDFilter is an abstraction for creating filter by resource id. +func ResourceIDFilter(id *provider.ResourceId) *link.ListPublicSharesRequest_Filter { + return &link.ListPublicSharesRequest_Filter{ + Type: link.ListPublicSharesRequest_Filter_TYPE_RESOURCE_ID, + Term: &link.ListPublicSharesRequest_Filter_ResourceId{ + ResourceId: id, + }, + } +} diff --git a/pkg/share/manager/json/json.go b/pkg/share/manager/json/json.go index 79f3ae3646..fe47803f94 100644 --- a/pkg/share/manager/json/json.go +++ b/pkg/share/manager/json/json.go @@ -354,7 +354,7 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference return nil, errtypes.NotFound(ref.String()) } -func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListSharesRequest_Filter) ([]*collaboration.Share, error) { +func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { var ss []*collaboration.Share m.Lock() defer m.Unlock() @@ -368,7 +368,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListShare // check filters // TODO(labkode): add the rest of filters. for _, f := range filters { - if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { if utils.ResourceIDEqual(s.ResourceId, f.GetResourceId()) { ss = append(ss, s) } @@ -381,7 +381,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListShare } // we list the shares that are targeted to the user in context or to the user groups. -func (m *mgr) ListReceivedShares(ctx context.Context) ([]*collaboration.ReceivedShare, error) { +func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { var rss []*collaboration.ReceivedShare m.Lock() defer m.Unlock() diff --git a/pkg/share/manager/memory/memory.go b/pkg/share/manager/memory/memory.go index df7e2b9a8e..8f84c77d9b 100644 --- a/pkg/share/manager/memory/memory.go +++ b/pkg/share/manager/memory/memory.go @@ -215,7 +215,7 @@ func (m *manager) UpdateShare(ctx context.Context, ref *collaboration.ShareRefer return nil, errtypes.NotFound(ref.String()) } -func (m *manager) ListShares(ctx context.Context, filters []*collaboration.ListSharesRequest_Filter) ([]*collaboration.Share, error) { +func (m *manager) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { var ss []*collaboration.Share m.lock.Lock() defer m.lock.Unlock() @@ -229,7 +229,7 @@ func (m *manager) ListShares(ctx context.Context, filters []*collaboration.ListS // check filters // TODO(labkode): add the rest of filters. for _, f := range filters { - if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { if utils.ResourceIDEqual(s.ResourceId, f.GetResourceId()) { ss = append(ss, s) } @@ -242,7 +242,7 @@ func (m *manager) ListShares(ctx context.Context, filters []*collaboration.ListS } // we list the shares that are targeted to the user in context or to the user groups. -func (m *manager) ListReceivedShares(ctx context.Context) ([]*collaboration.ReceivedShare, error) { +func (m *manager) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { var rss []*collaboration.ReceivedShare m.lock.Lock() defer m.lock.Unlock() diff --git a/pkg/share/manager/sql/sql.go b/pkg/share/manager/sql/sql.go index 6fea507391..c43ef2b377 100644 --- a/pkg/share/manager/sql/sql.go +++ b/pkg/share/manager/sql/sql.go @@ -269,13 +269,13 @@ func (m *mgr) UpdateShare(ctx context.Context, ref *collaboration.ShareReference return m.GetShare(ctx, ref) } -func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListSharesRequest_Filter) ([]*collaboration.Share, error) { +func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) { uid := ctxpkg.ContextMustGetUser(ctx).Username query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(item_source, '') as item_source, id, stime, permissions, share_type FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=? OR share_type=?)" var filterQuery string params := []interface{}{uid, uid, 0, 1} for i, f := range filters { - if f.Type == collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID { + if f.Type == collaboration.Filter_TYPE_RESOURCE_ID { filterQuery += "(item_source=?)" if i != len(filters)-1 { filterQuery += " AND " @@ -315,7 +315,7 @@ func (m *mgr) ListShares(ctx context.Context, filters []*collaboration.ListShare } // we list the shares that are targeted to the user in context or to the user groups. -func (m *mgr) ListReceivedShares(ctx context.Context) ([]*collaboration.ReceivedShare, error) { +func (m *mgr) ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { user := ctxpkg.ContextMustGetUser(ctx) uid := user.Username diff --git a/pkg/share/manager/sql/sql_test.go b/pkg/share/manager/sql/sql_test.go index fd61639fa6..aba406862a 100644 --- a/pkg/share/manager/sql/sql_test.go +++ b/pkg/share/manager/sql/sql_test.go @@ -165,20 +165,15 @@ var _ = Describe("SQL manager", func() { Describe("ListShares", func() { It("lists shares", func() { - shares, err := mgr.ListShares(ctx, []*collaboration.ListSharesRequest_Filter{}) + shares, err := mgr.ListShares(ctx, []*collaboration.Filter{}) Expect(err).ToNot(HaveOccurred()) Expect(len(shares)).To(Equal(1)) - shares, err = mgr.ListShares(ctx, []*collaboration.ListSharesRequest_Filter{ - { - Type: collaboration.ListSharesRequest_Filter_TYPE_RESOURCE_ID, - Term: &collaboration.ListSharesRequest_Filter_ResourceId{ - ResourceId: &provider.ResourceId{ - StorageId: "/", - OpaqueId: "somethingElse", - }, - }, - }, + shares, err = mgr.ListShares(ctx, []*collaboration.Filter{ + share.ResourceIDFilter(&provider.ResourceId{ + StorageId: "/", + OpaqueId: "somethingElse", + }), }) Expect(err).ToNot(HaveOccurred()) Expect(len(shares)).To(Equal(0)) @@ -188,7 +183,7 @@ var _ = Describe("SQL manager", func() { Describe("ListReceivedShares", func() { It("lists received shares", func() { loginAs(otherUser) - shares, err := mgr.ListReceivedShares(ctx) + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) Expect(err).ToNot(HaveOccurred()) Expect(len(shares)).To(Equal(1)) }) @@ -230,7 +225,7 @@ var _ = Describe("SQL manager", func() { Describe("Unshare", func() { It("deletes shares", func() { loginAs(otherUser) - shares, err := mgr.ListReceivedShares(ctx) + shares, err := mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) Expect(err).ToNot(HaveOccurred()) Expect(len(shares)).To(Equal(1)) @@ -243,7 +238,7 @@ var _ = Describe("SQL manager", func() { Expect(err).ToNot(HaveOccurred()) loginAs(otherUser) - shares, err = mgr.ListReceivedShares(ctx) + shares, err = mgr.ListReceivedShares(ctx, []*collaboration.Filter{}) Expect(err).ToNot(HaveOccurred()) Expect(len(shares)).To(Equal(0)) }) diff --git a/pkg/share/share.go b/pkg/share/share.go index b66bdf8969..9184e0d93c 100644 --- a/pkg/share/share.go +++ b/pkg/share/share.go @@ -41,10 +41,10 @@ type Manager interface { // 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 []*collaboration.ListSharesRequest_Filter) ([]*collaboration.Share, error) + ListShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.Share, error) // ListReceivedShares returns the list of shares the user has access. - ListReceivedShares(ctx context.Context) ([]*collaboration.ReceivedShare, error) + ListReceivedShares(ctx context.Context, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) // GetReceivedShare returns the information for a received share the user has access. GetReceivedShare(ctx context.Context, ref *collaboration.ShareReference) (*collaboration.ReceivedShare, error) @@ -52,3 +52,33 @@ type Manager interface { // UpdateReceivedShare updates the received share with share state. UpdateReceivedShare(ctx context.Context, ref *collaboration.ShareReference, f *collaboration.UpdateReceivedShareRequest_UpdateField) (*collaboration.ReceivedShare, error) } + +// GroupGranteeFilter is an abstraction for creating filter by grantee type group. +func GroupGranteeFilter() *collaboration.Filter { + return &collaboration.Filter{ + Type: collaboration.Filter_TYPE_GRANTEE_TYPE, + Term: &collaboration.Filter_GranteeType{ + GranteeType: provider.GranteeType_GRANTEE_TYPE_GROUP, + }, + } +} + +// UserGranteeFilter is an abstraction for creating filter by grantee type user. +func UserGranteeFilter() *collaboration.Filter { + return &collaboration.Filter{ + Type: collaboration.Filter_TYPE_GRANTEE_TYPE, + Term: &collaboration.Filter_GranteeType{ + GranteeType: provider.GranteeType_GRANTEE_TYPE_USER, + }, + } +} + +// ResourceIDFilter is an abstraction for creating filter by resource id. +func ResourceIDFilter(id *provider.ResourceId) *collaboration.Filter { + return &collaboration.Filter{ + Type: collaboration.Filter_TYPE_RESOURCE_ID, + Term: &collaboration.Filter_ResourceId{ + ResourceId: id, + }, + } +} diff --git a/pkg/storage/fs/nextcloud/nextcloud.go b/pkg/storage/fs/nextcloud/nextcloud.go index f7de3758af..4920679eba 100644 --- a/pkg/storage/fs/nextcloud/nextcloud.go +++ b/pkg/storage/fs/nextcloud/nextcloud.go @@ -30,6 +30,8 @@ import ( user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/storage" "github.com/cs3org/reva/pkg/storage/fs/registry" @@ -71,16 +73,25 @@ func New(m map[string]interface{}) (storage.FS, error) { return nil, err } - return NewStorageDriver(conf, nil) + return NewStorageDriver(conf) +} + +// CreateStorageSpace creates a storage space +func (nc *StorageDriver) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") } // NewStorageDriver returns a new NextcloudStorageDriver -func NewStorageDriver(c *StorageDriverConfig, client *http.Client) (*StorageDriver, error) { - if client == nil { +func NewStorageDriver(c *StorageDriverConfig) (*StorageDriver, error) { + var client *http.Client + if c.MockHTTP { + nextcloudServerMock := GetNextcloudServerMock() + client, _ = TestingHTTPClient(nextcloudServerMock) + } else { client = &http.Client{} } return &StorageDriver{ - endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/~alice/" + endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" client: client, }, nil } @@ -91,6 +102,15 @@ type Action struct { argS string } +func getUser(ctx context.Context) (*user.User, error) { + u, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx") + return nil, err + } + return u, nil +} + // SetHTTPClient sets the HTTP client func (nc *StorageDriver) SetHTTPClient(c *http.Client) { nc.client = c @@ -120,33 +140,50 @@ func (nc *StorageDriver) doUpload(r io.ReadCloser) error { return err } -func (nc *StorageDriver) do(a Action, endPoint string) (int, []byte, error) { - url := endPoint + a.verb +func (nc *StorageDriver) do(ctx context.Context, a Action) (int, []byte, error) { + log := appctx.GetLogger(ctx) + user, err := getUser(ctx) + if err != nil { + return 0, nil, err + } + url := nc.endPoint + "~" + user.Username + "/api/" + a.verb + log.Info().Msgf("nc.do %s", url) req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) if err != nil { - panic(err) + return 0, nil, err } req.Header.Set("Content-Type", "application/json") resp, err := nc.client.Do(req) if err != nil { - panic(err) + return 0, nil, err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) - return resp.StatusCode, body, err + if err != nil { + return 0, nil, err + } + + log.Info().Msgf("nc.do response %d %s", resp.StatusCode, body) + return resp.StatusCode, body, nil } // GetHome as defined in the storage.FS interface func (nc *StorageDriver) GetHome(ctx context.Context) (string, error) { - _, respBody, err := nc.do(Action{"GetHome", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("GetHome") + + _, respBody, err := nc.do(ctx, Action{"GetHome", ""}) return string(respBody), err } // CreateHome as defined in the storage.FS interface func (nc *StorageDriver) CreateHome(ctx context.Context) error { - _, _, err := nc.do(Action{"CreateHome", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("CreateHome") + + _, _, err := nc.do(ctx, Action{"CreateHome", ""}) return err } @@ -156,7 +193,10 @@ func (nc *StorageDriver) CreateDir(ctx context.Context, ref *provider.Reference) if err != nil { return err } - _, _, err = nc.do(Action{"CreateDir", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("CreateDir %s", bodyStr) + + _, _, err = nc.do(ctx, Action{"CreateDir", string(bodyStr)}) return err } @@ -166,7 +206,10 @@ func (nc *StorageDriver) Delete(ctx context.Context, ref *provider.Reference) er if err != nil { return err } - _, _, err = nc.do(Action{"Delete", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("Delete %s", bodyStr) + + _, _, err = nc.do(ctx, Action{"Delete", string(bodyStr)}) return err } @@ -176,17 +219,23 @@ func (nc *StorageDriver) Move(ctx context.Context, oldRef, newRef *provider.Refe data["from"] = oldRef.Path data["to"] = newRef.Path bodyStr, _ := json.Marshal(data) - _, _, err := nc.do(Action{"Move", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("Move %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"Move", string(bodyStr)}) return err } // GetMD as defined in the storage.FS interface func (nc *StorageDriver) GetMD(ctx context.Context, ref *provider.Reference, mdKeys []string) (*provider.ResourceInfo, error) { bodyStr, err := json.Marshal(ref) + log := appctx.GetLogger(ctx) + log.Info().Msgf("GetMD %s", bodyStr) + if err != nil { return nil, err } - status, body, err := nc.do(Action{"GetMD", string(bodyStr)}, nc.endPoint) + status, body, err := nc.do(ctx, Action{"GetMD", string(bodyStr)}) if err != nil { return nil, err } @@ -237,10 +286,12 @@ func (nc *StorageDriver) GetMD(ctx context.Context, ref *provider.Reference, mdK // ListFolder as defined in the storage.FS interface func (nc *StorageDriver) ListFolder(ctx context.Context, ref *provider.Reference, mdKeys []string) ([]*provider.ResourceInfo, error) { bodyStr, err := json.Marshal(ref) + log := appctx.GetLogger(ctx) + log.Info().Msgf("LisfFolder %s", bodyStr) if err != nil { return nil, err } - status, body, err := nc.do(Action{"ListFolder", string(bodyStr)}, nc.endPoint) + status, body, err := nc.do(ctx, Action{"ListFolder", string(bodyStr)}) if err != nil { return nil, err } @@ -277,7 +328,10 @@ func (nc *StorageDriver) ListFolder(ctx context.Context, ref *provider.Reference // InitiateUpload as defined in the storage.FS interface func (nc *StorageDriver) InitiateUpload(ctx context.Context, ref *provider.Reference, uploadLength int64, metadata map[string]string) (map[string]string, error) { bodyStr, _ := json.Marshal(ref) - _, respBody, err := nc.do(Action{"InitiateUpload", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("InitiateUpload %s", bodyStr) + + _, respBody, err := nc.do(ctx, Action{"InitiateUpload", string(bodyStr)}) if err != nil { return nil, err } @@ -292,25 +346,34 @@ func (nc *StorageDriver) InitiateUpload(ctx context.Context, ref *provider.Refer // Upload as defined in the storage.FS interface func (nc *StorageDriver) Upload(ctx context.Context, ref *provider.Reference, r io.ReadCloser) error { bodyStr, _ := json.Marshal(ref) + log := appctx.GetLogger(ctx) + log.Info().Msgf("Upload %s", bodyStr) + err := nc.doUpload(r) if err != nil { return err } - _, _, err = nc.do(Action{"Upload", string(bodyStr)}, nc.endPoint) + _, _, err = nc.do(ctx, Action{"Upload", string(bodyStr)}) return err } // Download as defined in the storage.FS interface func (nc *StorageDriver) Download(ctx context.Context, ref *provider.Reference) (io.ReadCloser, error) { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"Download", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("Download %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"Download", string(bodyStr)}) return nil, err } // ListRevisions as defined in the storage.FS interface func (nc *StorageDriver) ListRevisions(ctx context.Context, ref *provider.Reference) ([]*provider.FileVersion, error) { bodyStr, _ := json.Marshal(ref) - _, respBody, err := nc.do(Action{"ListRevisions", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("ListRevisions %s", bodyStr) + + _, respBody, err := nc.do(ctx, Action{"ListRevisions", string(bodyStr)}) if err != nil { return nil, err } @@ -338,20 +401,29 @@ func (nc *StorageDriver) ListRevisions(ctx context.Context, ref *provider.Refere // DownloadRevision as defined in the storage.FS interface func (nc *StorageDriver) DownloadRevision(ctx context.Context, ref *provider.Reference, key string) (io.ReadCloser, error) { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"DownloadRevision", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("DownloadRevision %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"DownloadRevision", string(bodyStr)}) return nil, err } // RestoreRevision as defined in the storage.FS interface func (nc *StorageDriver) RestoreRevision(ctx context.Context, ref *provider.Reference, key string) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"RestoreRevision", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("RestoreRevision %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"RestoreRevision", string(bodyStr)}) return err } // ListRecycle as defined in the storage.FS interface func (nc *StorageDriver) ListRecycle(ctx context.Context, key string, path string) ([]*provider.RecycleItem, error) { - _, respBody, err := nc.do(Action{"ListRecycle", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("ListRecycle") + _, respBody, err := nc.do(ctx, Action{"ListRecycle", ""}) + if err != nil { return nil, err } @@ -386,62 +458,89 @@ func (nc *StorageDriver) ListRecycle(ctx context.Context, key string, path strin // RestoreRecycleItem as defined in the storage.FS interface func (nc *StorageDriver) RestoreRecycleItem(ctx context.Context, key string, path string, restoreRef *provider.Reference) error { bodyStr, _ := json.Marshal(restoreRef) - _, _, err := nc.do(Action{"RestoreRecycleItem", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("RestoreRecycleItem %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"RestoreRecycleItem", string(bodyStr)}) return err } // PurgeRecycleItem as defined in the storage.FS interface func (nc *StorageDriver) PurgeRecycleItem(ctx context.Context, key string, path string) error { bodyStr, _ := json.Marshal(key) - _, _, err := nc.do(Action{"PurgeRecycleItem", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("PurgeRecycleItem %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"PurgeRecycleItem", string(bodyStr)}) return err } // EmptyRecycle as defined in the storage.FS interface func (nc *StorageDriver) EmptyRecycle(ctx context.Context) error { - _, _, err := nc.do(Action{"EmptyRecycle", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("EmptyRecycle") + + _, _, err := nc.do(ctx, Action{"EmptyRecycle", ""}) return err } // GetPathByID as defined in the storage.FS interface func (nc *StorageDriver) GetPathByID(ctx context.Context, id *provider.ResourceId) (string, error) { bodyStr, _ := json.Marshal(id) - _, respBody, err := nc.do(Action{"GetPathByID", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("GetPathByID %s", bodyStr) + + _, respBody, err := nc.do(ctx, Action{"GetPathByID", string(bodyStr)}) return string(respBody), err } // AddGrant as defined in the storage.FS interface func (nc *StorageDriver) AddGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"AddGrant", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("AggGrant %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"AddGrant", string(bodyStr)}) return err } // RemoveGrant as defined in the storage.FS interface func (nc *StorageDriver) RemoveGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"RemoveGrant", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("RemoveGrant %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"RemoveGrant", string(bodyStr)}) return err } // DenyGrant as defined in the storage.FS interface func (nc *StorageDriver) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"DenyGrant", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("DenyGrant %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"DenyGrant", string(bodyStr)}) return err } // UpdateGrant as defined in the storage.FS interface func (nc *StorageDriver) UpdateGrant(ctx context.Context, ref *provider.Reference, g *provider.Grant) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"UpdateGrant", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("UpdateGrant %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"UpdateGrant", string(bodyStr)}) return err } // ListGrants as defined in the storage.FS interface func (nc *StorageDriver) ListGrants(ctx context.Context, ref *provider.Reference) ([]*provider.Grant, error) { bodyStr, _ := json.Marshal(ref) - _, respBody, err := nc.do(Action{"ListGrants", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("ListGrants %s", bodyStr) + + _, respBody, err := nc.do(ctx, Action{"ListGrants", string(bodyStr)}) if err != nil { return nil, err } @@ -506,38 +605,56 @@ func (nc *StorageDriver) ListGrants(ctx context.Context, ref *provider.Reference // GetQuota as defined in the storage.FS interface func (nc *StorageDriver) GetQuota(ctx context.Context) (uint64, uint64, error) { - _, _, err := nc.do(Action{"GetQuota", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("GetQuota") + + _, _, err := nc.do(ctx, Action{"GetQuota", ""}) return 0, 0, err } // CreateReference as defined in the storage.FS interface func (nc *StorageDriver) CreateReference(ctx context.Context, path string, targetURI *url.URL) error { - _, _, err := nc.do(Action{"CreateReference", fmt.Sprintf(`{"path":"%s"}`, path)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("CreateReference %s", path) + + _, _, err := nc.do(ctx, Action{"CreateReference", fmt.Sprintf(`{"path":"%s"}`, path)}) return err } // Shutdown as defined in the storage.FS interface func (nc *StorageDriver) Shutdown(ctx context.Context) error { - _, _, err := nc.do(Action{"Shutdown", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("Shutdown") + + _, _, err := nc.do(ctx, Action{"Shutdown", ""}) return err } // SetArbitraryMetadata as defined in the storage.FS interface func (nc *StorageDriver) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error { bodyStr, _ := json.Marshal(md) - _, _, err := nc.do(Action{"SetArbitraryMetadata", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("SetArbitraryMetadata %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"SetArbitraryMetadata", string(bodyStr)}) return err } // UnsetArbitraryMetadata as defined in the storage.FS interface func (nc *StorageDriver) UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error { bodyStr, _ := json.Marshal(ref) - _, _, err := nc.do(Action{"UnsetArbitraryMetadata", string(bodyStr)}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msgf("UnsetArbitraryMetadata %s", bodyStr) + + _, _, err := nc.do(ctx, Action{"UnsetArbitraryMetadata", string(bodyStr)}) return err } // ListStorageSpaces :as defined in the storage.FS interface func (nc *StorageDriver) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { - _, _, err := nc.do(Action{"ListStorageSpaces", ""}, nc.endPoint) + log := appctx.GetLogger(ctx) + log.Info().Msg("ListStorageSpaces") + + _, _, err := nc.do(ctx, Action{"ListStorageSpaces", ""}) return nil, err } diff --git a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go index 1b438894e6..a38420f41b 100644 --- a/pkg/storage/fs/nextcloud/nextcloud_server_mock.go +++ b/pkg/storage/fs/nextcloud/nextcloud_server_mock.go @@ -19,9 +19,12 @@ package nextcloud import ( + "context" "fmt" "io" + "net" "net/http" + "net/http/httptest" "strings" ) @@ -49,86 +52,86 @@ const serverStateMetadata = "METADATA" var serverState = serverStateEmpty var responses = map[string]Response{ - `POST /apps/sciencemesh/~alice/AddGrant {"path":"/subdir"}`: {200, ``, serverStateGrantAdded}, + `POST /apps/sciencemesh/~einstein/api/AddGrant {"path":"/subdir"}`: {200, ``, serverStateGrantAdded}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/subdir"} EMPTY`: {200, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/subdir"} HOME`: {200, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/subdir"} NEWDIR`: {200, ``, serverStateSubdirNewdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/subdir"} EMPTY`: {200, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/subdir"} HOME`: {200, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/subdir"} NEWDIR`: {200, ``, serverStateSubdirNewdir}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/newdir"} EMPTY`: {200, ``, serverStateNewdir}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/newdir"} HOME`: {200, ``, serverStateNewdir}, - `POST /apps/sciencemesh/~alice/CreateDir {"path":"/newdir"} SUBDIR`: {200, ``, serverStateSubdirNewdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/newdir"} EMPTY`: {200, ``, serverStateNewdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/newdir"} HOME`: {200, ``, serverStateNewdir}, + `POST /apps/sciencemesh/~einstein/api/CreateDir {"path":"/newdir"} SUBDIR`: {200, ``, serverStateSubdirNewdir}, - `POST /apps/sciencemesh/~alice/CreateHome `: {200, ``, serverStateHome}, - `POST /apps/sciencemesh/~alice/CreateHome {}`: {200, ``, serverStateHome}, + `POST /apps/sciencemesh/~einstein/api/CreateHome `: {200, ``, serverStateHome}, + `POST /apps/sciencemesh/~einstein/api/CreateHome {}`: {200, ``, serverStateHome}, - `POST /apps/sciencemesh/~alice/CreateReference {"path":"/Shares/reference"}`: {200, `[]`, serverStateReference}, + `POST /apps/sciencemesh/~einstein/api/CreateReference {"path":"/Shares/reference"}`: {200, `[]`, serverStateReference}, - `POST /apps/sciencemesh/~alice/Delete {"path":"/subdir"}`: {200, ``, serverStateRecycle}, + `POST /apps/sciencemesh/~einstein/api/Delete {"path":"/subdir"}`: {200, ``, serverStateRecycle}, - `POST /apps/sciencemesh/~alice/EmptyRecycle `: {200, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/EmptyRecycle `: {200, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/"} EMPTY`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/"} HOME`: {200, `{ "size": 1 }`, serverStateHome}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/"} EMPTY`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/"} HOME`: {200, `{ "size": 1 }`, serverStateHome}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/newdir"} EMPTY`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/newdir"} HOME`: {404, ``, serverStateHome}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/newdir"} SUBDIR`: {404, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/newdir"} NEWDIR`: {200, `{ "size": 1 }`, serverStateNewdir}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/newdir"} SUBDIR-NEWDIR`: {200, `{ "size": 1 }`, serverStateSubdirNewdir}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/newdir"} EMPTY`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/newdir"} HOME`: {404, ``, serverStateHome}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/newdir"} SUBDIR`: {404, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/newdir"} NEWDIR`: {200, `{ "size": 1 }`, serverStateNewdir}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/newdir"} SUBDIR-NEWDIR`: {200, `{ "size": 1 }`, serverStateSubdirNewdir}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/new_subdir"}`: {200, `{ "size": 1 }`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/new_subdir"}`: {200, `{ "size": 1 }`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} EMPTY`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} HOME`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} NEWDIR`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} RECYCLE`: {404, ``, serverStateRecycle}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} SUBDIR`: {200, `{ "size": 1 }`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} SUBDIR-NEWDIR`: {200, `{ "size": 1 }`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdir"} METADATA`: {200, `{ "size": 1, "metadata": { "foo": "bar" } }`, serverStateMetadata}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} EMPTY`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} HOME`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} NEWDIR`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} RECYCLE`: {404, ``, serverStateRecycle}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} SUBDIR`: {200, `{ "size": 1 }`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} SUBDIR-NEWDIR`: {200, `{ "size": 1 }`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdir"} METADATA`: {200, `{ "size": 1, "metadata": { "foo": "bar" } }`, serverStateMetadata}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdirRestored"} EMPTY`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdirRestored"} RECYCLE`: {404, ``, serverStateRecycle}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdirRestored"} SUBDIR`: {404, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/subdirRestored"} FILE-RESTORED`: {200, `{ "size": 1 }`, serverStateFileRestored}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdirRestored"} EMPTY`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdirRestored"} RECYCLE`: {404, ``, serverStateRecycle}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdirRestored"} SUBDIR`: {404, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/subdirRestored"} FILE-RESTORED`: {200, `{ "size": 1 }`, serverStateFileRestored}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/versionedFile"} EMPTY`: {200, `{ "size": 2 }`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/GetMD {"path":"/versionedFile"} FILE-RESTORED`: {200, `{ "size": 1 }`, serverStateFileRestored}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/versionedFile"} EMPTY`: {200, `{ "size": 2 }`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetMD {"path":"/versionedFile"} FILE-RESTORED`: {200, `{ "size": 1 }`, serverStateFileRestored}, - `POST /apps/sciencemesh/~alice/GetPathByID {"storage_id":"00000000-0000-0000-0000-000000000000","opaque_id":"fileid-%2Fsubdir"}`: {200, "/subdir", serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/GetPathByID {"storage_id":"00000000-0000-0000-0000-000000000000","opaque_id":"fileid-%2Fsubdir"}`: {200, "/subdir", serverStateEmpty}, - `POST /apps/sciencemesh/~alice/InitiateUpload {"path":"/file"}`: {200, `{"simple": "yes","tus": "yes"}`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/InitiateUpload {"path":"/file"}`: {200, `{"simple": "yes","tus": "yes"}`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListFolder {"path":"/"}`: {200, `["/subdir"]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListFolder {"path":"/"}`: {200, `["/subdir"]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListFolder {"path":"/Shares"} EMPTY`: {404, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListFolder {"path":"/Shares"} SUBDIR`: {404, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/ListFolder {"path":"/Shares"} REFERENCE`: {200, `["reference"]`, serverStateReference}, + `POST /apps/sciencemesh/~einstein/api/ListFolder {"path":"/Shares"} EMPTY`: {404, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListFolder {"path":"/Shares"} SUBDIR`: {404, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/ListFolder {"path":"/Shares"} REFERENCE`: {200, `["reference"]`, serverStateReference}, - `POST /apps/sciencemesh/~alice/ListGrants {"path":"/subdir"} SUBDIR`: {200, `[]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListGrants {"path":"/subdir"} GRANT-ADDED`: {200, `[ { "stat": true, "move": true, "delete": false } ]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListGrants {"path":"/subdir"} GRANT-UPDATED`: {200, `[ { "stat": true, "move": true, "delete": true } ]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListGrants {"path":"/subdir"} SUBDIR`: {200, `[]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListGrants {"path":"/subdir"} GRANT-ADDED`: {200, `[ { "stat": true, "move": true, "delete": false } ]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListGrants {"path":"/subdir"} GRANT-UPDATED`: {200, `[ { "stat": true, "move": true, "delete": true } ]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListRecycle EMPTY`: {200, `[]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListRecycle RECYCLE`: {200, `["/subdir"]`, serverStateRecycle}, + `POST /apps/sciencemesh/~einstein/api/ListRecycle EMPTY`: {200, `[]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListRecycle RECYCLE`: {200, `["/subdir"]`, serverStateRecycle}, - `POST /apps/sciencemesh/~alice/ListRevisions {"path":"/versionedFile"} EMPTY`: {500, `[1]`, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/ListRevisions {"path":"/versionedFile"} FILE-RESTORED`: {500, `[1, 2]`, serverStateFileRestored}, + `POST /apps/sciencemesh/~einstein/api/ListRevisions {"path":"/versionedFile"} EMPTY`: {500, `[1]`, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/ListRevisions {"path":"/versionedFile"} FILE-RESTORED`: {500, `[1, 2]`, serverStateFileRestored}, - `POST /apps/sciencemesh/~alice/Move {"from":"/subdir","to":"/new_subdir"}`: {200, ``, serverStateEmpty}, + `POST /apps/sciencemesh/~einstein/api/Move {"from":"/subdir","to":"/new_subdir"}`: {200, ``, serverStateEmpty}, - `POST /apps/sciencemesh/~alice/RemoveGrant {"path":"/subdir"} GRANT-ADDED`: {200, ``, serverStateGrantUpdated}, + `POST /apps/sciencemesh/~einstein/api/RemoveGrant {"path":"/subdir"} GRANT-ADDED`: {200, ``, serverStateGrantUpdated}, - `POST /apps/sciencemesh/~alice/RestoreRecycleItem null`: {200, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/RestoreRecycleItem {"path":"/subdirRestored"}`: {200, ``, serverStateFileRestored}, + `POST /apps/sciencemesh/~einstein/api/RestoreRecycleItem null`: {200, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/RestoreRecycleItem {"path":"/subdirRestored"}`: {200, ``, serverStateFileRestored}, - `POST /apps/sciencemesh/~alice/RestoreRevision {"path":"/versionedFile"}`: {200, ``, serverStateFileRestored}, + `POST /apps/sciencemesh/~einstein/api/RestoreRevision {"path":"/versionedFile"}`: {200, ``, serverStateFileRestored}, - `POST /apps/sciencemesh/~alice/SetArbitraryMetadata {"metadata":{"foo":"bar"}}`: {200, ``, serverStateMetadata}, + `POST /apps/sciencemesh/~einstein/api/SetArbitraryMetadata {"metadata":{"foo":"bar"}}`: {200, ``, serverStateMetadata}, - `POST /apps/sciencemesh/~alice/UnsetArbitraryMetadata {"path":"/subdir"}`: {200, ``, serverStateSubdir}, + `POST /apps/sciencemesh/~einstein/api/UnsetArbitraryMetadata {"path":"/subdir"}`: {200, ``, serverStateSubdir}, - `POST /apps/sciencemesh/~alice/UpdateGrant {"path":"/subdir"}`: {200, ``, serverStateGrantUpdated}, + `POST /apps/sciencemesh/~einstein/api/UpdateGrant {"path":"/subdir"}`: {200, ``, serverStateGrantUpdated}, } // GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server @@ -166,3 +169,23 @@ func GetNextcloudServerMock() http.Handler { } }) } + +// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2 +// Ideally, this function would live in tests/helpers, but +// if we put it there, it gets excluded by .dockerignore, and the +// Docker build fails (see https://github.com/cs3org/reva/issues/1999) +// So putting it here for now - open to suggestions if someone knows +// a better way to inject this. +func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { + s := httptest.NewServer(handler) + + cli := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, s.Listener.Addr().String()) + }, + }, + } + + return cli, s.Close +} diff --git a/pkg/storage/fs/nextcloud/nextcloud_test.go b/pkg/storage/fs/nextcloud/nextcloud_test.go index a8c83d8d17..3f990e5266 100644 --- a/pkg/storage/fs/nextcloud/nextcloud_test.go +++ b/pkg/storage/fs/nextcloud/nextcloud_test.go @@ -23,7 +23,13 @@ import ( "net/http" "os" + "google.golang.org/grpc/metadata" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + "github.com/cs3org/reva/pkg/auth/scope" + ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/storage/fs/nextcloud" + jwt "github.com/cs3org/reva/pkg/token/manager/jwt" "github.com/cs3org/reva/tests/helpers" . "github.com/onsi/ginkgo" @@ -32,11 +38,20 @@ import ( var _ = Describe("Nextcloud", func() { var ( + ctx context.Context options map[string]interface{} tmpRoot string + user = &userpb.User{ + Id: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + } ) BeforeEach(func() { + var err error tmpRoot, err := helpers.TempDir("reva-unit-tests-*-root") Expect(err).ToNot(HaveOccurred()) @@ -45,6 +60,19 @@ var _ = Describe("Nextcloud", func() { "enable_home": true, "share_folder": "/Shares", } + + ctx = context.Background() + + // Add auth token + tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"}) + Expect(err).ToNot(HaveOccurred()) + scope, err := scope.AddOwnerScope(nil) + Expect(err).ToNot(HaveOccurred()) + t, err := tokenManager.MintToken(ctx, user, scope) + Expect(err).ToNot(HaveOccurred()) + ctx = ctxpkg.ContextSetToken(ctx, t) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, t) + ctx = ctxpkg.ContextSetUser(ctx, user) }) AfterEach(func() { @@ -61,12 +89,10 @@ var _ = Describe("Nextcloud", func() { }) Describe("CreateHome", func() { It("calls the CreateHome endpoint", func() { - nextcloudServerMock := nextcloud.GetNextcloudServerMock() - var client *http.Client - client, _ = helpers.TestingHTTPClient(nextcloudServerMock) nc, _ := nextcloud.NewStorageDriver(&nextcloud.StorageDriverConfig{ EndPoint: "http://mock.com", - }, client) + MockHTTP: true, + }) const ( okResponse = `{ @@ -82,10 +108,10 @@ var _ = Describe("Nextcloud", func() { panic(err) } }) - mock, teardown := helpers.TestingHTTPClient(h) + mock, teardown := nextcloud.TestingHTTPClient(h) defer teardown() nc.SetHTTPClient(mock) - err2 := nc.CreateHome(context.TODO()) + err2 := nc.CreateHome(ctx) Expect(err2).ToNot(HaveOccurred()) }) }) diff --git a/pkg/storage/fs/owncloud/owncloud.go b/pkg/storage/fs/owncloud/owncloud.go index 8eba17ca41..e2bcebdd17 100644 --- a/pkg/storage/fs/owncloud/owncloud.go +++ b/pkg/storage/fs/owncloud/owncloud.go @@ -705,6 +705,11 @@ func getResourceType(isDir bool) provider.ResourceType { return provider.ResourceType_RESOURCE_TYPE_FILE } +// CreateStorageSpace creates a storage space +func (fs *ocfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") +} + func readOrCreateID(ctx context.Context, ip string, conn redis.Conn) string { log := appctx.GetLogger(ctx) diff --git a/pkg/storage/fs/owncloud/owncloud_unix.go b/pkg/storage/fs/owncloud/owncloud_unix.go index 11c82f1f6c..0bb88308c8 100755 --- a/pkg/storage/fs/owncloud/owncloud_unix.go +++ b/pkg/storage/fs/owncloud/owncloud_unix.go @@ -16,6 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +//go:build !windows // +build !windows package owncloud diff --git a/pkg/storage/fs/owncloud/owncloud_windows.go b/pkg/storage/fs/owncloud/owncloud_windows.go index 5002351480..69dbbeddb3 100644 --- a/pkg/storage/fs/owncloud/owncloud_windows.go +++ b/pkg/storage/fs/owncloud/owncloud_windows.go @@ -16,6 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +//go:build windows // +build windows package owncloud diff --git a/pkg/storage/fs/owncloudsql/owncloudsql.go b/pkg/storage/fs/owncloudsql/owncloudsql.go index 744f835c19..4c776c34bf 100644 --- a/pkg/storage/fs/owncloudsql/owncloudsql.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql.go @@ -489,6 +489,11 @@ func (fs *owncloudsqlfs) getUserStorage(user string) (int, error) { return id, err } +// CreateStorageSpace creates a storage space +func (fs *owncloudsqlfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") +} + func (fs *owncloudsqlfs) convertToResourceInfo(ctx context.Context, entry *filecache.File, ip string, mdKeys []string) (*provider.ResourceInfo, error) { mdKeysMap := make(map[string]struct{}) for _, k := range mdKeys { diff --git a/pkg/storage/fs/owncloudsql/owncloudsql_unix.go b/pkg/storage/fs/owncloudsql/owncloudsql_unix.go index 9e5c1ad9ff..a63f712df6 100755 --- a/pkg/storage/fs/owncloudsql/owncloudsql_unix.go +++ b/pkg/storage/fs/owncloudsql/owncloudsql_unix.go @@ -16,6 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +//go:build !windows // +build !windows package owncloudsql diff --git a/pkg/storage/fs/s3/s3.go b/pkg/storage/fs/s3/s3.go index eaf717562a..f1522deb11 100644 --- a/pkg/storage/fs/s3/s3.go +++ b/pkg/storage/fs/s3/s3.go @@ -383,6 +383,11 @@ func (fs *s3FS) Delete(ctx context.Context, ref *provider.Reference) error { return nil } +// CreateStorageSpace creates a storage space +func (fs *s3FS) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") +} + func (fs *s3FS) moveObject(ctx context.Context, oldKey string, newKey string) error { // Copy diff --git a/pkg/storage/storage.go b/pkg/storage/storage.go index 1b65c30fdd..df873db0c9 100644 --- a/pkg/storage/storage.go +++ b/pkg/storage/storage.go @@ -58,6 +58,7 @@ type FS interface { SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error UnsetArbitraryMetadata(ctx context.Context, ref *provider.Reference, keys []string) error ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) + CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) } // Registry is the interface that storage registries implement diff --git a/pkg/storage/utils/decomposedfs/decomposedfs.go b/pkg/storage/utils/decomposedfs/decomposedfs.go index 3bf1bfb2c8..f57731c847 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs.go @@ -24,7 +24,6 @@ package decomposedfs import ( "context" "io" - "math" "net/url" "os" "path" @@ -33,9 +32,7 @@ import ( "strings" "syscall" - userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/appctx" ctxpkg "github.com/cs3org/reva/pkg/ctx" "github.com/cs3org/reva/pkg/errtypes" @@ -226,27 +223,6 @@ func (fs *Decomposedfs) CreateHome(ctx context.Context) (err error) { return } -func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType, nodeID string) error { - - // create space type dir - if err := os.MkdirAll(filepath.Join(fs.o.Root, "spaces", spaceType), 0700); err != nil { - return err - } - - // we can reuse the node id as the space id - err := os.Symlink("../../nodes/"+nodeID, filepath.Join(fs.o.Root, "spaces", spaceType, nodeID)) - if err != nil { - if isAlreadyExists(err) { - appctx.GetLogger(ctx).Debug().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("symlink already exists") - } else { - // TODO how should we handle error cases here? - appctx.GetLogger(ctx).Error().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("could not create symlink") - } - } - - return nil -} - // The os not exists error is buried inside the xattr error, // so we cannot just use os.IsNotExists(). func isAlreadyExists(err error) bool { @@ -522,158 +498,6 @@ func (fs *Decomposedfs) Download(ctx context.Context, ref *provider.Reference) ( return reader, nil } -// ListStorageSpaces returns a list of StorageSpaces. -// The list can be filtered by space type or space id. -// Spaces are persisted with symlinks in /spaces// pointing to ../../nodes/, the root node of the space -// The spaceid is a concatenation of storageid + "!" + nodeid -func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { - // TODO check filters - - // TODO when a space symlink is broken delete the space for cleanup - // read permissions are deduced from the node? - - // TODO for absolute references this actually requires us to move all user homes into a subfolder of /nodes/root, - // e.g. /nodes/root/ otherwise storage space names might collide even though they are of different types - // /nodes/root/personal/foo and /nodes/root/shares/foo might be two very different spaces, a /nodes/root/foo is not expressive enough - // we would not need /nodes/root if access always happened via spaceid+relative path - - var ( - spaceType = "*" - spaceID = "*" - ) - - for i := range filter { - switch filter[i].Type { - case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE: - spaceType = filter[i].GetSpaceType() - case provider.ListStorageSpacesRequest_Filter_TYPE_ID: - parts := strings.SplitN(filter[i].GetId().OpaqueId, "!", 2) - if len(parts) == 2 { - spaceID = parts[1] - } - } - } - - // build the glob path, eg. - // /path/to/root/spaces/personal/nodeid - // /path/to/root/spaces/shared/nodeid - matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceType, spaceID)) - if err != nil { - return nil, err - } - - spaces := make([]*provider.StorageSpace, 0, len(matches)) - - u, ok := ctxpkg.ContextGetUser(ctx) - if !ok { - appctx.GetLogger(ctx).Debug().Msg("expected user in context") - return spaces, nil - } - - for i := range matches { - // always read link in case storage space id != node id - if target, err := os.Readlink(matches[i]); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[i]).Msg("could not read link, skipping") - continue - } else { - n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Str("id", filepath.Base(target)).Msg("could not read node, skipping") - continue - } - owner, err := n.Owner() - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read owner, skipping") - continue - } - - // TODO apply more filters - - space := &provider.StorageSpace{ - // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers - //Id: &provider.StorageSpaceId{OpaqueId: "1284d238-aa92-42ce-bdc4-0b0000009157!" + n.ID}, - Root: &provider.ResourceId{ - // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers - //StorageId: "1284d238-aa92-42ce-bdc4-0b0000009157", - OpaqueId: n.ID, - }, - Name: n.Name, - SpaceType: filepath.Base(filepath.Dir(matches[i])), - // Mtime is set either as node.tmtime or as fi.mtime below - } - - if space.SpaceType == "share" { - if utils.UserEqual(u.Id, owner) { - // do not list shares as spaces for the owner - continue - } - } else { - space.Name = "root" // do not expose the id as name, this is the root of a space - // TODO read from extended attribute for project / group spaces - } - - // filter out spaces user cannot access (currently based on stat permission) - p, err := n.ReadUserPermissions(ctx, u) - if err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read permissions, skipping") - continue - } - if !p.Stat { - continue - } - - // fill in user object if the current user is the owner - if utils.UserEqual(u.Id, owner) { - space.Owner = u - } else { - space.Owner = &userv1beta1.User{ // FIXME only return a UserID, not a full blown user object - Id: owner, - } - } - - // we set the space mtime to the root item mtime - // override the stat mtime with a tmtime if it is present - if tmt, err := n.GetTMTime(); err == nil { - un := tmt.UnixNano() - space.Mtime = &types.Timestamp{ - Seconds: uint64(un / 1000000000), - Nanos: uint32(un % 1000000000), - } - } else if fi, err := os.Stat(matches[i]); err == nil { - // fall back to stat mtime - un := fi.ModTime().UnixNano() - space.Mtime = &types.Timestamp{ - Seconds: uint64(un / 1000000000), - Nanos: uint32(un % 1000000000), - } - } - - // quota - v, err := xattr.Get(matches[i], xattrs.QuotaAttr) - if err == nil { - // make sure we have a proper signed int - // we use the same magic numbers to indicate: - // -1 = uncalculated - // -2 = unknown - // -3 = unlimited - if quota, err := strconv.ParseUint(string(v), 10, 64); err == nil { - space.Quota = &provider.Quota{ - QuotaMaxBytes: quota, - QuotaMaxFiles: math.MaxUint64, // TODO MaxUInt64? = unlimited? why even max files? 0 = unlimited? - } - } else { - appctx.GetLogger(ctx).Debug().Err(err).Str("nodepath", matches[i]).Msg("could not read quota") - } - } - - spaces = append(spaces, space) - } - } - - return spaces, nil - -} - func (fs *Decomposedfs) copyMD(s string, t string) (err error) { var attrs []string if attrs, err = xattr.List(s); err != nil { diff --git a/pkg/storage/utils/decomposedfs/decomposedfs_unix.go b/pkg/storage/utils/decomposedfs/decomposedfs_unix.go index 3cc758ec1b..d5b7507002 100644 --- a/pkg/storage/utils/decomposedfs/decomposedfs_unix.go +++ b/pkg/storage/utils/decomposedfs/decomposedfs_unix.go @@ -16,6 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +//go:build !windows // +build !windows package decomposedfs diff --git a/pkg/storage/utils/decomposedfs/grants.go b/pkg/storage/utils/decomposedfs/grants.go index f630f2f0fb..2bcc900bd0 100644 --- a/pkg/storage/utils/decomposedfs/grants.go +++ b/pkg/storage/utils/decomposedfs/grants.go @@ -32,6 +32,9 @@ import ( "github.com/pkg/xattr" ) +// SpaceGrant is the key used to signal not to create a new space when a grant is assigned to a storage space. +var SpaceGrant struct{} + // DenyGrant denies access to a resource. func (fs *Decomposedfs) DenyGrant(ctx context.Context, ref *provider.Reference, g *provider.Grantee) error { return errtypes.NotSupported("decomposedfs: not supported") @@ -68,8 +71,12 @@ func (fs *Decomposedfs) AddGrant(ctx context.Context, ref *provider.Reference, g return err } - if err := fs.createStorageSpace(ctx, "share", node.ID); err != nil { - return err + // when a grant is added to a space, do not add a new space under "shares" + if spaceGrant := ctx.Value(SpaceGrant); spaceGrant == nil { + err := fs.createStorageSpace(ctx, "share", node.ID) + if err != nil { + return err + } } return fs.tp.Propagate(ctx, node) diff --git a/pkg/storage/utils/decomposedfs/node/node.go b/pkg/storage/utils/decomposedfs/node/node.go index d9807e3f01..3b67fc7af4 100644 --- a/pkg/storage/utils/decomposedfs/node/node.go +++ b/pkg/storage/utils/decomposedfs/node/node.go @@ -120,6 +120,16 @@ func (n *Node) ChangeOwner(new *userpb.UserId) (err error) { return } +// SetMetadata populates a given key with its value. +// Note that consumers should be aware of the metadata options on xattrs.go. +func (n *Node) SetMetadata(key string, val string) (err error) { + nodePath := n.InternalPath() + if err := xattr.Set(nodePath, key, []byte(val)); err != nil { + return errors.Wrap(err, "Decomposedfs: could not set parentid attribute") + } + return nil +} + // WriteMetadata writes the Node metadata to disk func (n *Node) WriteMetadata(owner *userpb.UserId) (err error) { nodePath := n.InternalPath() diff --git a/pkg/storage/utils/decomposedfs/spaces.go b/pkg/storage/utils/decomposedfs/spaces.go new file mode 100644 index 0000000000..d26babf4f6 --- /dev/null +++ b/pkg/storage/utils/decomposedfs/spaces.go @@ -0,0 +1,323 @@ +// 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 decomposedfs + +import ( + "context" + "fmt" + "math" + "os" + "path/filepath" + "strconv" + "strings" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + v1beta11 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + ocsconv "github.com/cs3org/reva/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/pkg/appctx" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/node" + "github.com/cs3org/reva/pkg/storage/utils/decomposedfs/xattrs" + "github.com/cs3org/reva/pkg/utils" + "github.com/google/uuid" + "github.com/pkg/errors" + "github.com/pkg/xattr" +) + +// CreateStorageSpace creates a storage space +func (fs *Decomposedfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + // spaces will be located by default in the root of the storage. + r, err := fs.lu.RootNode(ctx) + if err != nil { + return nil, err + } + + // "everything is a resource" this is the unique ID for the Space resource. + spaceID := uuid.New().String() + + n, err := r.Child(ctx, spaceID) + if err != nil { + return nil, err + } + + if n.Exists { + return nil, fmt.Errorf("decomposedfs: spaces: invalid duplicated node with id `%s`", n.ID) + } + + if err := fs.tp.CreateDir(ctx, n); err != nil { + return nil, err + } + + if err := fs.createHiddenSpaceFolder(ctx, n); err != nil { + return nil, err + } + + u, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + return nil, fmt.Errorf("decomposedfs: spaces: contextual user not found") + } + + if err := n.ChangeOwner(u.Id); err != nil { + return nil, err + } + + err = fs.createStorageSpace(ctx, "project", n.ID) + if err != nil { + return nil, err + } + + // set default space quota + if err := n.SetMetadata(xattrs.QuotaAttr, strconv.FormatUint(req.GetQuota().QuotaMaxBytes, 10)); err != nil { + return nil, err + } + + if err := n.SetMetadata(xattrs.SpaceNameAttr, req.Name); err != nil { + return nil, err + } + + resp := &provider.CreateStorageSpaceResponse{ + Status: &v1beta11.Status{ + Code: v1beta11.Code_CODE_OK, + }, + StorageSpace: &provider.StorageSpace{ + Owner: u, + Id: &provider.StorageSpaceId{ + OpaqueId: spaceID, + }, + // TODO we have to omit Root information because the storage driver does not know its mount point. + // Root: &provider.ResourceId{ + // StorageId: "", + // OpaqueId: "", + // }, + Name: req.GetName(), + Quota: req.GetQuota(), + SpaceType: req.GetType(), + }, + } + + nPath, err := fs.lu.Path(ctx, n) + if err != nil { + return nil, errors.Wrap(err, "decomposedfs: spaces: could not create space. invalid node path") + } + + ctx = context.WithValue(ctx, SpaceGrant, struct{}{}) + + if err := fs.AddGrant(ctx, &provider.Reference{ + Path: nPath, + }, &provider.Grant{ + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: u.Id, + }, + }, + Permissions: ocsconv.NewEditorRole().CS3ResourcePermissions(), + }); err != nil { + return nil, err + } + + return resp, nil +} + +// ListStorageSpaces returns a list of StorageSpaces. +// The list can be filtered by space type or space id. +// Spaces are persisted with symlinks in /spaces// pointing to ../../nodes/, the root node of the space +// The spaceid is a concatenation of storageid + "!" + nodeid +func (fs *Decomposedfs) ListStorageSpaces(ctx context.Context, filter []*provider.ListStorageSpacesRequest_Filter) ([]*provider.StorageSpace, error) { + // TODO check filters + + // TODO when a space symlink is broken delete the space for cleanup + // read permissions are deduced from the node? + + // TODO for absolute references this actually requires us to move all user homes into a subfolder of /nodes/root, + // e.g. /nodes/root/ otherwise storage space names might collide even though they are of different types + // /nodes/root/personal/foo and /nodes/root/shares/foo might be two very different spaces, a /nodes/root/foo is not expressive enough + // we would not need /nodes/root if access always happened via spaceid+relative path + + var ( + spaceType = "*" + spaceID = "*" + ) + + for i := range filter { + switch filter[i].Type { + case provider.ListStorageSpacesRequest_Filter_TYPE_SPACE_TYPE: + spaceType = filter[i].GetSpaceType() + case provider.ListStorageSpacesRequest_Filter_TYPE_ID: + parts := strings.SplitN(filter[i].GetId().OpaqueId, "!", 2) + if len(parts) == 2 { + spaceID = parts[1] + } + } + } + + // build the glob path, eg. + // /path/to/root/spaces/personal/nodeid + // /path/to/root/spaces/shared/nodeid + matches, err := filepath.Glob(filepath.Join(fs.o.Root, "spaces", spaceType, spaceID)) + if err != nil { + return nil, err + } + + spaces := make([]*provider.StorageSpace, 0, len(matches)) + + u, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + appctx.GetLogger(ctx).Debug().Msg("expected user in context") + return spaces, nil + } + + for i := range matches { + // always read link in case storage space id != node id + if target, err := os.Readlink(matches[i]); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("match", matches[i]).Msg("could not read link, skipping") + continue + } else { + n, err := node.ReadNode(ctx, fs.lu, filepath.Base(target)) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Str("id", filepath.Base(target)).Msg("could not read node, skipping") + continue + } + owner, err := n.Owner() + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read owner, skipping") + continue + } + + // TODO apply more filters + + space := &provider.StorageSpace{ + // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers + //Id: &provider.StorageSpaceId{OpaqueId: "1284d238-aa92-42ce-bdc4-0b0000009157!" + n.ID}, + Root: &provider.ResourceId{ + // FIXME the driver should know its id move setting the spaceid from the storage provider to the drivers + //StorageId: "1284d238-aa92-42ce-bdc4-0b0000009157", + OpaqueId: n.ID, + }, + Name: n.Name, + SpaceType: filepath.Base(filepath.Dir(matches[i])), + // Mtime is set either as node.tmtime or as fi.mtime below + } + + if space.SpaceType == "share" { + if utils.UserEqual(u.Id, owner) { + // do not list shares as spaces for the owner + continue + } + } else { + space.Name = "root" // do not expose the id as name, this is the root of a space + // TODO read from extended attribute for project / group spaces + } + + // filter out spaces user cannot access (currently based on stat permission) + p, err := n.ReadUserPermissions(ctx, u) + if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("node", n).Msg("could not read permissions, skipping") + continue + } + if !p.Stat { + continue + } + + // fill in user object if the current user is the owner + if utils.UserEqual(u.Id, owner) { + space.Owner = u + } else { + space.Owner = &userv1beta1.User{ // FIXME only return a UserID, not a full blown user object + Id: owner, + } + } + + // we set the space mtime to the root item mtime + // override the stat mtime with a tmtime if it is present + if tmt, err := n.GetTMTime(); err == nil { + un := tmt.UnixNano() + space.Mtime = &types.Timestamp{ + Seconds: uint64(un / 1000000000), + Nanos: uint32(un % 1000000000), + } + } else if fi, err := os.Stat(matches[i]); err == nil { + // fall back to stat mtime + un := fi.ModTime().UnixNano() + space.Mtime = &types.Timestamp{ + Seconds: uint64(un / 1000000000), + Nanos: uint32(un % 1000000000), + } + } + + // quota + v, err := xattr.Get(matches[i], xattrs.QuotaAttr) + if err == nil { + // make sure we have a proper signed int + // we use the same magic numbers to indicate: + // -1 = uncalculated + // -2 = unknown + // -3 = unlimited + if quota, err := strconv.ParseUint(string(v), 10, 64); err == nil { + space.Quota = &provider.Quota{ + QuotaMaxBytes: quota, + QuotaMaxFiles: math.MaxUint64, // TODO MaxUInt64? = unlimited? why even max files? 0 = unlimited? + } + } else { + appctx.GetLogger(ctx).Debug().Err(err).Str("nodepath", matches[i]).Msg("could not read quota") + } + } + + spaces = append(spaces, space) + } + } + + return spaces, nil + +} + +// createHiddenSpaceFolder bootstraps a storage space root with a hidden ".space" folder used to store space related +// metadata such as a description or an image. +// Internally createHiddenSpaceFolder leverages the use of node.Child() to create a new node under the space root. +// createHiddenSpaceFolder is just a contextual alias for node.Child() for ".spaces". +func (fs *Decomposedfs) createHiddenSpaceFolder(ctx context.Context, r *node.Node) error { + hiddenSpace, err := r.Child(ctx, ".space") + if err != nil { + return err + } + + return fs.tp.CreateDir(ctx, hiddenSpace) +} + +func (fs *Decomposedfs) createStorageSpace(ctx context.Context, spaceType, nodeID string) error { + // create space type dir + if err := os.MkdirAll(filepath.Join(fs.o.Root, "spaces", spaceType), 0700); err != nil { + return err + } + + // we can reuse the node id as the space id + err := os.Symlink("../../nodes/"+nodeID, filepath.Join(fs.o.Root, "spaces", spaceType, nodeID)) + if err != nil { + if isAlreadyExists(err) { + appctx.GetLogger(ctx).Debug().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("symlink already exists") + } else { + // TODO how should we handle error cases here? + appctx.GetLogger(ctx).Error().Err(err).Str("node", nodeID).Str("spacetype", spaceType).Msg("could not create symlink") + } + } + + return nil +} diff --git a/pkg/storage/utils/decomposedfs/xattrs/xattrs.go b/pkg/storage/utils/decomposedfs/xattrs/xattrs.go index db8296980d..6a8dcc5ef3 100644 --- a/pkg/storage/utils/decomposedfs/xattrs/xattrs.go +++ b/pkg/storage/utils/decomposedfs/xattrs/xattrs.go @@ -40,7 +40,8 @@ const ( OwnerTypeAttr string = OcisPrefix + "owner.type" // the base name of the node // updated when the file is renamed or moved - NameAttr string = OcisPrefix + "name" + NameAttr string = OcisPrefix + "name" + BlobIDAttr string = OcisPrefix + "blobid" BlobsizeAttr string = OcisPrefix + "blobsize" @@ -77,6 +78,9 @@ const ( // the quota for the storage space / tree, regardless who accesses it QuotaAttr string = OcisPrefix + "quota" + // the name given to a storage space. It should not contain any semantics as its only purpose is to be read. + SpaceNameAttr string = OcisPrefix + "space.name" + UserAcePrefix string = "u:" GroupAcePrefix string = "g:" ) diff --git a/pkg/storage/utils/eosfs/eosfs.go b/pkg/storage/utils/eosfs/eosfs.go index 73b66c9b71..88319c3214 100644 --- a/pkg/storage/utils/eosfs/eosfs.go +++ b/pkg/storage/utils/eosfs/eosfs.go @@ -968,6 +968,11 @@ func (fs *eosfs) listShareFolderRoot(ctx context.Context, p string) (finfos []*p return finfos, nil } +// CreateStorageSpace creates a storage space +func (fs *eosfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") +} + func (fs *eosfs) GetQuota(ctx context.Context) (uint64, uint64, error) { u, err := getUser(ctx) if err != nil { diff --git a/pkg/storage/utils/localfs/localfs.go b/pkg/storage/utils/localfs/localfs.go index 2af306dde0..e35d18601b 100644 --- a/pkg/storage/utils/localfs/localfs.go +++ b/pkg/storage/utils/localfs/localfs.go @@ -564,6 +564,11 @@ func (fs *localfs) CreateReference(ctx context.Context, path string, targetURI * return fs.propagate(ctx, fn) } +// CreateStorageSpace creates a storage space +func (fs *localfs) CreateStorageSpace(ctx context.Context, req *provider.CreateStorageSpaceRequest) (*provider.CreateStorageSpaceResponse, error) { + return nil, fmt.Errorf("unimplemented: CreateStorageSpace") +} + func (fs *localfs) SetArbitraryMetadata(ctx context.Context, ref *provider.Reference, md *provider.ArbitraryMetadata) error { np, err := fs.resolve(ctx, ref) diff --git a/pkg/storage/utils/localfs/localfs_unix.go b/pkg/storage/utils/localfs/localfs_unix.go index abcf56553f..29ea2224af 100644 --- a/pkg/storage/utils/localfs/localfs_unix.go +++ b/pkg/storage/utils/localfs/localfs_unix.go @@ -16,6 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. +//go:build !windows // +build !windows package localfs diff --git a/pkg/user/manager/ldap/ldap.go b/pkg/user/manager/ldap/ldap.go index 575bfcd0d3..c2dcde670f 100644 --- a/pkg/user/manager/ldap/ldap.go +++ b/pkg/user/manager/ldap/ldap.go @@ -424,12 +424,12 @@ func (m *manager) getUserFilter(uid *userpb.UserId) string { } func (m *manager) getAttributeFilter(attribute, value string) string { - attr := strings.ReplaceAll(m.c.AttributeFilter, "{{attr}}", attribute) - return strings.ReplaceAll(attr, "{{value}}", value) + attr := strings.ReplaceAll(m.c.AttributeFilter, "{{attr}}", ldap.EscapeFilter(attribute)) + return strings.ReplaceAll(attr, "{{value}}", ldap.EscapeFilter(value)) } func (m *manager) getFindFilter(query string) string { - return strings.ReplaceAll(m.c.FindFilter, "{{query}}", query) + return strings.ReplaceAll(m.c.FindFilter, "{{query}}", ldap.EscapeFilter(query)) } func (m *manager) getGroupFilter(uid *userpb.UserId) string { diff --git a/pkg/user/manager/loader/loader.go b/pkg/user/manager/loader/loader.go index 41e61a9a6f..7640c3ff31 100644 --- a/pkg/user/manager/loader/loader.go +++ b/pkg/user/manager/loader/loader.go @@ -23,6 +23,7 @@ import ( _ "github.com/cs3org/reva/pkg/user/manager/demo" _ "github.com/cs3org/reva/pkg/user/manager/json" _ "github.com/cs3org/reva/pkg/user/manager/ldap" + _ "github.com/cs3org/reva/pkg/user/manager/nextcloud" _ "github.com/cs3org/reva/pkg/user/manager/owncloudsql" // Add your own here ) diff --git a/pkg/user/manager/nextcloud/nextcloud.go b/pkg/user/manager/nextcloud/nextcloud.go new file mode 100644 index 0000000000..08219fdbda --- /dev/null +++ b/pkg/user/manager/nextcloud/nextcloud.go @@ -0,0 +1,155 @@ +// 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 nextcloud + +import ( + "context" + "io" + "net/http" + "strings" + + "github.com/cs3org/reva/pkg/user" + "github.com/cs3org/reva/pkg/user/manager/registry" + "github.com/mitchellh/mapstructure" + "github.com/pkg/errors" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + // "github.com/cs3org/reva/pkg/errtypes" +) + +func init() { + registry.Register("nextcloud", New) +} + +type manager struct { + client *http.Client + endPoint string +} + +type config struct { + EndPoint string `mapstructure:"end_point"` + MockHTTP bool `mapstructure:"mock_http"` +} + +func (c *config) init() { + if c.EndPoint == "" { + c.EndPoint = "http://localhost/end/point?" + } +} + +func parseConfig(m map[string]interface{}) (*config, error) { + c := &config{} + if err := mapstructure.Decode(m, c); err != nil { + err = errors.Wrap(err, "error decoding conf") + return nil, err + } + c.init() + return c, nil +} + +// Action describes a REST request to forward to the Nextcloud backend +type Action struct { + verb string + argS string +} + +// New returns a user manager implementation that reads a json file to provide user metadata. +func New(m map[string]interface{}) (user.Manager, error) { + c, err := parseConfig(m) + if err != nil { + return nil, err + } + + return &manager{ + client: &http.Client{}, + endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" + }, nil +} + +func (m *manager) do(a Action) (int, []byte, error) { + url := m.endPoint + a.verb + req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) + if err != nil { + panic(err) + } + + req.Header.Set("Content-Type", "application/json") + resp, err := m.client.Do(req) + if err != nil { + panic(err) + } + + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + return resp.StatusCode, body, err +} + +func (m *manager) Configure(ml map[string]interface{}) error { + return nil +} + +func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User, error) { + _, respBody, err := m.do(Action{"GetUser", uid.Idp}) + u := &userpb.User{ + Username: string(respBody), + } + return u, err +} + +func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, error) { + _, respBody, err := m.do(Action{"FindUsers", query}) + u := &userpb.User{ + Username: string(respBody), + } + var us = make([]*userpb.User, 1) + us[0] = u + return us, err +} + +func (m *manager) GetUserByClaim(ctx context.Context, claim, value string) (*userpb.User, error) { + _, respBody, err := m.do(Action{"GetUserByClaim", value}) + u := &userpb.User{ + Username: string(respBody), + } + return u, err +} + +// func extractClaim(u *userpb.User, claim string) (string, error) { +// _, respBody, err := m.do(Action{"ExtractClaim", claim}) +// u := &userpb.User{ +// Username: string(respBody), +// } +// return u, err +// } + +// func userContains(u *userpb.User, query string) bool { +// _, respBody, err := m.do(Action{"userContains", query}) +// u := &userpb.User{ +// Username: string(respBody), +// } +// return u, err +// return false +// } + +func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { + _, respBody, err := m.do(Action{"GetUserGroups", uid.Idp}) + var gs = make([]string, 1) + gs[0] = string(respBody) + return gs, err +} diff --git a/pkg/user/manager/registry/registry.go b/pkg/user/manager/registry/registry.go index f9f2477a41..b436e0da0c 100644 --- a/pkg/user/manager/registry/registry.go +++ b/pkg/user/manager/registry/registry.go @@ -18,7 +18,11 @@ package registry -import "github.com/cs3org/reva/pkg/user" +import ( + "fmt" + + "github.com/cs3org/reva/pkg/user" +) // NewFunc is the function that user managers // should register at init time. @@ -30,5 +34,6 @@ var NewFuncs = map[string]NewFunc{} // Register registers a new user manager new function. // Not safe for concurrent use. Safe for use from package init. func Register(name string, f NewFunc) { + fmt.Printf("Got registration for user manager %s\n", name) NewFuncs[name] = f } diff --git a/tests/acceptance/expected-failures-on-OCIS-storage.md b/tests/acceptance/expected-failures-on-OCIS-storage.md index 25ba737c35..844697ad18 100644 --- a/tests/acceptance/expected-failures-on-OCIS-storage.md +++ b/tests/acceptance/expected-failures-on-OCIS-storage.md @@ -327,7 +327,6 @@ File and sync features in a shared scenario #### [Response is empty when accepting a share](https://github.com/owncloud/product/issues/207) -- [apiShareManagementToShares/acceptShares.feature:82](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L82) - [apiShareManagementToShares/acceptShares.feature:207](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L207) - [apiShareManagementToShares/acceptShares.feature:261](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L261) diff --git a/tests/acceptance/expected-failures-on-S3NG-storage.md b/tests/acceptance/expected-failures-on-S3NG-storage.md index 423d8aa226..cbd5ac6dde 100644 --- a/tests/acceptance/expected-failures-on-S3NG-storage.md +++ b/tests/acceptance/expected-failures-on-S3NG-storage.md @@ -330,7 +330,6 @@ File and sync features in a shared scenario #### [Response is empty when accepting a share](https://github.com/owncloud/product/issues/207) -- [apiShareManagementToShares/acceptShares.feature:82](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L82) - [apiShareManagementToShares/acceptShares.feature:207](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L207) - [apiShareManagementToShares/acceptShares.feature:261](https://github.com/owncloud/core/blob/master/tests/acceptance/features/apiShareManagementToShares/acceptShares.feature#L261) diff --git a/tests/helpers/helpers.go b/tests/helpers/helpers.go index a7dc7c9bb0..6b6ba37248 100644 --- a/tests/helpers/helpers.go +++ b/tests/helpers/helpers.go @@ -19,11 +19,7 @@ package helpers import ( - "context" "io/ioutil" - "net" - "net/http" - "net/http/httptest" "os" "path/filepath" "runtime" @@ -48,18 +44,3 @@ func TempDir(name string) (string, error) { return tmpRoot, nil } - -// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2 -func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { - s := httptest.NewServer(handler) - - cli := &http.Client{ - Transport: &http.Transport{ - DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { - return net.Dial(network, s.Listener.Addr().String()) - }, - }, - } - - return cli, s.Close -} diff --git a/tests/integration/grpc/fixtures/storageprovider-nextcloud.toml b/tests/integration/grpc/fixtures/storageprovider-nextcloud.toml index 99ae4c85bd..26d3b127ba 100644 --- a/tests/integration/grpc/fixtures/storageprovider-nextcloud.toml +++ b/tests/integration/grpc/fixtures/storageprovider-nextcloud.toml @@ -5,5 +5,5 @@ address = "{{grpc_address}}" driver = "nextcloud" [grpc.services.storageprovider.drivers.nextcloud] -end_point = "http://localhost:8080/apps/sciencemesh/~alice/" +end_point = "http://localhost:8080/apps/sciencemesh/" mock_http = true diff --git a/tests/integration/grpc/storageprovider_test.go b/tests/integration/grpc/storageprovider_test.go index fc09c751d4..9c19da71cd 100644 --- a/tests/integration/grpc/storageprovider_test.go +++ b/tests/integration/grpc/storageprovider_test.go @@ -61,6 +61,7 @@ var _ = Describe("storage providers", func() { OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", Type: userpb.UserType_USER_TYPE_PRIMARY, }, + Username: "einstein", } homeRef = &storagep.Reference{Path: "/"} @@ -465,7 +466,7 @@ var _ = Describe("storage providers", func() { }) } - PDescribe("nextcloud", func() { + Describe("nextcloud", func() { BeforeEach(func() { dependencies = map[string]string{ "storage": "storageprovider-nextcloud.toml",