diff --git a/Makefile b/Makefile index 9248e55eb5..bb6f12cbec 100644 --- a/Makefile +++ b/Makefile @@ -76,6 +76,9 @@ imports: off $(GOIMPORTS) .PHONY: build build: build-revad build-reva test-go-version +.PHONY: build-debug +build-debug: build-revad-debug build-reva-debug test-go-version + .PHONY: build-cephfs build-cephfs: build-revad-cephfs build-reva @@ -87,6 +90,10 @@ tidy: build-revad: imports go build -ldflags ${BUILD_FLAGS} -o ./cmd/revad/revad ./cmd/revad +.PHONY: build-revad-debug +build-revad-debug: imports + go build -gcflags="all=-N -l" -ldflags ${BUILD_FLAGS} -o ./cmd/revad/revad ./cmd/revad + .PHONY: build-revad-cephfs build-revad-cephfs: imports go build -ldflags ${BUILD_FLAGS} -tags ceph -o ./cmd/revad/revad ./cmd/revad @@ -95,6 +102,10 @@ build-revad-cephfs: imports build-reva: imports go build -ldflags ${BUILD_FLAGS} -o ./cmd/reva/reva ./cmd/reva +.PHONY: build-reva-debug +build-reva-debug: imports + go build -gcflags="all=-N -l" -ldflags ${BUILD_FLAGS} -o ./cmd/reva/reva ./cmd/reva + # to be run in Docker build .PHONY: build-revad-docker build-revad-docker: off diff --git a/changelog/unreleased/ocm.md b/changelog/unreleased/ocm.md new file mode 100644 index 0000000000..9c81c22c07 --- /dev/null +++ b/changelog/unreleased/ocm.md @@ -0,0 +1,5 @@ +Enhancement: Port OCM changes from master + +We pulled in the latest ocm changes from master and are now compatible with the main go-cs3apis again. + +https://github.com/cs3org/reva/pull/4239 diff --git a/changelog/unreleased/reva-token-bearer.md b/changelog/unreleased/reva-token-bearer.md new file mode 100644 index 0000000000..224d6cfe69 --- /dev/null +++ b/changelog/unreleased/reva-token-bearer.md @@ -0,0 +1,3 @@ +Enhancement: Accept reva token as a bearer authentication + +https://github.com/cs3org/reva/pull/3315 diff --git a/cmd/reva/app-tokens-create.go b/cmd/reva/app-tokens-create.go index 20231f6073..eaab4ab6af 100644 --- a/cmd/reva/app-tokens-create.go +++ b/cmd/reva/app-tokens-create.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -73,7 +73,6 @@ func appTokensCreateCommand() *command { } cmd.Action = func(w ...io.Writer) error { - createOpts := &appTokenCreateOpts{ Expiration: *expiration, Label: *label, @@ -215,7 +214,7 @@ func getPathScope(ctx context.Context, client gateway.GatewayAPIClient, path, pe return scope.AddResourceInfoScope(statResponse.GetInfo(), role, scopes) } -// parse permission string in the form of "rw" to create a role +// parse permission string in the form of "rw" to create a role. func parsePermission(perm string) (authpb.Role, error) { switch perm { case "r": diff --git a/cmd/reva/app-tokens-list.go b/cmd/reva/app-tokens-list.go index d4e46a3d9e..3fe37cd108 100644 --- a/cmd/reva/app-tokens-list.go +++ b/cmd/reva/app-tokens-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -38,7 +38,6 @@ func appTokensListCommand() *command { cmd.Usage = func() string { return "Usage: token-list" } cmd.Action = func(w ...io.Writer) error { - client, err := getClient() if err != nil { return err diff --git a/cmd/reva/app-tokens-remove.go b/cmd/reva/app-tokens-remove.go index 8bd9334f0d..7d12ccf6df 100644 --- a/cmd/reva/app-tokens-remove.go +++ b/cmd/reva/app-tokens-remove.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/arguments.go b/cmd/reva/arguments.go index b618a159b5..7d1a4a84b0 100644 --- a/cmd/reva/arguments.go +++ b/cmd/reva/arguments.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -138,7 +138,7 @@ func (c *Completer) ocmShareReceivedArgumentCompleter() []prompt.Suggest { } for _, r := range info { - suggests = append(suggests, prompt.Suggest{Text: r.Share.Id.OpaqueId}) + suggests = append(suggests, prompt.Suggest{Text: r.Id.OpaqueId}) } } @@ -195,7 +195,6 @@ func (c *Completer) shareReceivedArgumentCompleter() []prompt.Suggest { } func executeCommand(cmd *command, args ...string) (bytes.Buffer, error) { - var b bytes.Buffer var err error diff --git a/cmd/reva/command.go b/cmd/reva/command.go index 9757704b17..4f609cb35a 100644 --- a/cmd/reva/command.go +++ b/cmd/reva/command.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,7 +24,7 @@ import ( "io" ) -// Command is the representation to create commands +// Command is the representation to create commands. type command struct { *flag.FlagSet Name string @@ -34,7 +34,7 @@ type command struct { ResetFlags func() } -// newCommand creates a new command +// newCommand creates a new command. func newCommand(name string) *command { fs := flag.NewFlagSet(name, flag.ExitOnError) cmd := &command{ diff --git a/cmd/reva/common.go b/cmd/reva/common.go index 69b6f3f9e0..c4c5c54cac 100644 --- a/cmd/reva/common.go +++ b/cmd/reva/common.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/completer.go b/cmd/reva/completer.go index 0b0bd90657..4df4519646 100644 --- a/cmd/reva/completer.go +++ b/cmd/reva/completer.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import ( "github.com/c-bata/go-prompt" ) -// Completer provides completion command handler +// Completer provides completion command handler. type Completer struct { Commands []*command DisableArgPrompt bool @@ -45,7 +45,7 @@ func (c *Completer) init() { c.lsDirArguments = new(argumentCompleter) } -// Complete provides completion to prompt +// Complete provides completion to prompt. func (c *Completer) Complete(d prompt.Document) []prompt.Suggest { if d.TextBeforeCursor() == "" { return []prompt.Suggest{} @@ -80,7 +80,6 @@ func (c *Completer) argumentCompleter(args ...string) []prompt.Suggest { } switch args[0] { - case "login": if len(args) == 2 { return prompt.FilterHasPrefix(c.loginArgumentCompleter(), args[1], true) diff --git a/cmd/reva/configure.go b/cmd/reva/configure.go index 5e6d042e30..07d0d0259e 100644 --- a/cmd/reva/configure.go +++ b/cmd/reva/configure.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/download.go b/cmd/reva/download.go index 1d373ea72a..7e60032811 100644 --- a/cmd/reva/download.go +++ b/cmd/reva/download.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ import ( "io" "net/http" "os" - "time" "github.com/cheggaaa/pb" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -50,7 +49,7 @@ func downloadCommand() *command { remote := cmd.Args()[0] local := cmd.Args()[1] - client, err := getClient() + gatewayClient, err := getClient() if err != nil { return err } @@ -58,7 +57,7 @@ func downloadCommand() *command { ref := &provider.Reference{Path: remote} req1 := &provider.StatRequest{Ref: ref} ctx := getAuthContext() - res1, err := client.Stat(ctx, req1) + res1, err := gatewayClient.Stat(ctx, req1) if err != nil { return err } @@ -71,7 +70,7 @@ func downloadCommand() *command { req2 := &provider.InitiateFileDownloadRequest{ Ref: &provider.Reference{Path: remote}, } - res, err := client.InitiateFileDownload(ctx, req2) + res, err := gatewayClient.InitiateFileDownload(ctx, req2) if err != nil { return err } @@ -96,21 +95,14 @@ func downloadCommand() *command { dataServerURL := p.DownloadEndpoint // TODO(labkode): do a protocol switch - httpReq, err := rhttp.NewRequest(ctx, "GET", dataServerURL, nil) + httpReq, err := rhttp.NewRequest(ctx, http.MethodGet, dataServerURL, nil) if err != nil { return err } httpReq.Header.Set(datagateway.TokenTransportHeader, p.Token) - httpClient := rhttp.GetHTTPClient( - rhttp.Context(ctx), - // TODO make insecure configurable - rhttp.Insecure(true), - // TODO make timeout configurable - rhttp.Timeout(time.Duration(24*int64(time.Hour))), - ) - - httpRes, err := httpClient.Do(httpReq) + + httpRes, err := client.Do(httpReq) if err != nil { return err } @@ -144,7 +136,10 @@ func downloadCommand() *command { return cmd } -func getDownloadProtocolInfo(protocolInfos []*gateway.FileDownloadProtocol, protocol string) (*gateway.FileDownloadProtocol, error) { +func getDownloadProtocolInfo( + protocolInfos []*gateway.FileDownloadProtocol, + protocol string, +) (*gateway.FileDownloadProtocol, error) { for _, p := range protocolInfos { if p.Protocol == protocol { return p, nil diff --git a/cmd/reva/executor.go b/cmd/reva/executor.go index 07150735db..86ee22aa91 100644 --- a/cmd/reva/executor.go +++ b/cmd/reva/executor.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,12 +28,12 @@ import ( "time" ) -// Executor provides exec command handler +// Executor provides exec command handler. type Executor struct { - Timeout int + Timeout int64 } -// Execute provides execute commands +// Execute provides execute commands. func (e *Executor) Execute(s string) { s = strings.TrimSpace(s) switch s { @@ -79,7 +79,7 @@ func (e *Executor) Execute(s string) { select { case <-signalChan: cancel() - case <-time.After(time.Duration(e.Timeout * int(time.Second))): + case <-time.After(time.Duration(e.Timeout * int64(time.Second))): cancel() case <-ctx.Done(): } @@ -109,7 +109,7 @@ func executeWithContext(ctx context.Context, cmd *command) error { }() select { case <-ctx.Done(): - return errors.New("Cancelled by user") + return errors.New("cancelled by user") case err := <-c: return err } diff --git a/cmd/reva/getlock.go b/cmd/reva/getlock.go index b4d501354c..79f0cb4e0e 100644 --- a/cmd/reva/getlock.go +++ b/cmd/reva/getlock.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/grpc.go b/cmd/reva/grpc.go index d254799124..b4bb8936b5 100644 --- a/cmd/reva/grpc.go +++ b/cmd/reva/grpc.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,12 +24,11 @@ import ( "fmt" "log" - "google.golang.org/grpc/credentials" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "google.golang.org/grpc" + "google.golang.org/grpc/credentials" ins "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/metadata" ) diff --git a/cmd/reva/help.go b/cmd/reva/help.go index fdc51430ea..bda9aadeab 100644 --- a/cmd/reva/help.go +++ b/cmd/reva/help.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/import.go b/cmd/reva/import.go index 8a9ff50d39..1211c601fa 100644 --- a/cmd/reva/import.go +++ b/cmd/reva/import.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/login.go b/cmd/reva/login.go index 20298d019a..3559be4c7b 100644 --- a/cmd/reva/login.go +++ b/cmd/reva/login.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/ls.go b/cmd/reva/ls.go index d2201058ae..b90f984b95 100644 --- a/cmd/reva/ls.go +++ b/cmd/reva/ls.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/main.go b/cmd/reva/main.go index 752fe40685..e19228a9db 100644 --- a/cmd/reva/main.go +++ b/cmd/reva/main.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,10 +31,10 @@ import ( ) var ( - conf *config - host string - insecure, skipverify, disableargprompt bool - timeout int + conf *config + host string + insecure, skipverify, disableargprompt, insecuredatagateway bool + timeout int64 helpCommandOutput string @@ -56,14 +56,17 @@ var ( moveCommand(), mkdirCommand(), ocmFindAcceptedUsersCommand(), + ocmRemoveAcceptedUser(), ocmInviteGenerateCommand(), ocmInviteForwardCommand(), ocmShareCreateCommand(), ocmShareListCommand(), ocmShareRemoveCommand(), ocmShareUpdateCommand(), + ocmShareGetCommand(), ocmShareListReceivedCommand(), ocmShareUpdateReceivedCommand(), + ocmShareGetReceivedCommand(), openInAppCommand(), preferencesCommand(), publicShareCreateCommand(), @@ -79,7 +82,6 @@ var ( shareUpdateCommand(), shareListReceivedCommand(), shareUpdateReceivedCommand(), - transferCreateCommand(), transferGetStatusCommand(), transferCancelCommand(), transferListCommand(), @@ -97,14 +99,24 @@ var ( func init() { flag.StringVar(&host, "host", "", "address of the GRPC gateway host") flag.BoolVar(&insecure, "insecure", false, "disables grpc transport security") - flag.BoolVar(&skipverify, "skip-verify", false, "whether to skip verifying the server's certificate chain and host name") + flag.BoolVar( + &insecuredatagateway, + "insecure-data-gateway", + false, + "disables grpc transport security for data gateway service", + ) + flag.BoolVar( + &skipverify, + "skip-verify", + false, + "whether to skip verifying the server's certificate chain and host name", + ) flag.BoolVar(&disableargprompt, "disable-arg-prompt", false, "whether to disable prompts for command arguments") - flag.IntVar(&timeout, "timout", -1, "the timeout in seconds for executing the commands, -1 means no timeout") + flag.Int64Var(&timeout, "timeout", -1, "the timeout in seconds for executing the commands, -1 means no timeout") flag.Parse() } func main() { - if host != "" { conf = &config{host} if err := writeConfig(conf); err != nil { @@ -114,10 +126,8 @@ func main() { } client = rhttp.GetHTTPClient( - // TODO make insecure configurable - rhttp.Insecure(true), - // TODO make timeout configurable - rhttp.Timeout(time.Duration(24*int64(time.Hour))), + rhttp.Insecure(insecuredatagateway), + rhttp.Timeout(time.Duration(timeout*int64(time.Second))), ) generateMainUsage() @@ -153,6 +163,11 @@ func generateMainUsage() { helpCommandOutput = "Command line interface to REVA:\n" for _, cmd := range commands { - helpCommandOutput += fmt.Sprintf("%s%s%s\n", cmd.Name, strings.Repeat(" ", 4+(n-len(cmd.Name))), cmd.Description()) + helpCommandOutput += fmt.Sprintf( + "%s%s%s\n", + cmd.Name, + strings.Repeat(" ", 4+(n-len(cmd.Name))), + cmd.Description(), + ) } } diff --git a/cmd/reva/mkdir.go b/cmd/reva/mkdir.go index c81e7560d6..0fbdce3205 100644 --- a/cmd/reva/mkdir.go +++ b/cmd/reva/mkdir.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/mv.go b/cmd/reva/mv.go index 72a9bd09ef..99f4b3ad66 100644 --- a/cmd/reva/mv.go +++ b/cmd/reva/mv.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/ocm-find-accepted-users.go b/cmd/reva/ocm-find-accepted-users.go index 5445f9f658..3a68246a79 100644 --- a/cmd/reva/ocm-find-accepted-users.go +++ b/cmd/reva/ocm-find-accepted-users.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/ocm-invite-forward.go b/cmd/reva/ocm-invite-forward.go index 219132fc75..913d0c1438 100644 --- a/cmd/reva/ocm-invite-forward.go +++ b/cmd/reva/ocm-invite-forward.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ func ocmInviteForwardCommand() *command { return formatError(providerInfo.Status) } - forwardToken, err := client.ForwardInvite(ctx, &invitepb.ForwardInviteRequest{ + res, err := client.ForwardInvite(ctx, &invitepb.ForwardInviteRequest{ InviteToken: inviteToken, OriginSystemProvider: providerInfo.ProviderInfo, }) @@ -77,10 +77,10 @@ func ocmInviteForwardCommand() *command { return err } - if forwardToken.Status.Code != rpc.Code_CODE_OK { - return formatError(forwardToken.Status) + if res.Status.Code != rpc.Code_CODE_OK { + return formatError(res.Status) } - fmt.Println("OK") + fmt.Println(res) return nil } return cmd diff --git a/cmd/reva/ocm-invite-generate.go b/cmd/reva/ocm-invite-generate.go index dc0e514a26..6763c92b13 100644 --- a/cmd/reva/ocm-invite-generate.go +++ b/cmd/reva/ocm-invite-generate.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/ocm-remove-accepted-user.go b/cmd/reva/ocm-remove-accepted-user.go new file mode 100644 index 0000000000..291454d074 --- /dev/null +++ b/cmd/reva/ocm-remove-accepted-user.go @@ -0,0 +1,77 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package main + +import ( + "errors" + "fmt" + "io" + + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" +) + +func ocmRemoveAcceptedUser() *command { + cmd := newCommand("ocm-remove-accepted-user") + cmd.Description = func() string { return "remove a remote user from the personal user list" } + cmd.Usage = func() string { return "Usage: ocm-remove-accepted-user [-flags]" } + + user := cmd.String("user", "", "the user id") + idp := cmd.String("idp", "", "the idp of the user") + + cmd.ResetFlags = func() { + *user, *idp = "", "" + } + + cmd.Action = func(w ...io.Writer) error { + // validate flags + if *user == "" { + return errors.New("User cannot be empty: user -user flag\n" + cmd.Usage()) + } + + if *idp == "" { + return errors.New("IdP cannot be empty: use -idp flag\n" + cmd.Usage()) + } + + ctx := getAuthContext() + client, err := getClient() + if err != nil { + return err + } + + res, err := client.DeleteAcceptedUser(ctx, &invitepb.DeleteAcceptedUserRequest{ + RemoteUserId: &userv1beta1.UserId{ + Type: userv1beta1.UserType_USER_TYPE_FEDERATED, + Idp: *idp, + OpaqueId: *user, + }, + }) + if err != nil { + return err + } + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + return formatError(res.Status) + } + + fmt.Println("OK") + return nil + } + return cmd +} diff --git a/cmd/reva/ocm-share-create.go b/cmd/reva/ocm-share-create.go index 29587bd59c..f183cdfb32 100644 --- a/cmd/reva/ocm-share-create.go +++ b/cmd/reva/ocm-share-create.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,25 +21,22 @@ package main import ( "io" "os" - "strconv" "time" + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" 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" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" + ocmshare "github.com/cs3org/reva/v2/pkg/ocm/share" "github.com/cs3org/reva/v2/pkg/utils" "github.com/jedib0t/go-pretty/table" "github.com/pkg/errors" ) -// default for resharing -var _resharing = false - func ocmShareCreateCommand() *command { cmd := newCommand("ocm-share-create") cmd.Description = func() string { return "create OCM share to a user or group" } @@ -48,27 +45,37 @@ func ocmShareCreateCommand() *command { grantee := cmd.String("grantee", "", "the grantee") idp := cmd.String("idp", "", "the idp of the grantee, default to same idp as the user triggering the action") userType := cmd.String("user-type", "primary", "the type of user account, defaults to primary") - rol := cmd.String("rol", "viewer", "the permission for the share (viewer or editor)") + spaceID := cmd.String("spaceid", "", "the space of the resource that's being shared") + opaqueID := cmd.String("id", "", "the id of the resource that's being shared") + + webdav := cmd.Bool("webdav", false, "create a share with webdav access") + webapp := cmd.Bool("webapp", false, "create a share for app access") + datatx := cmd.Bool("datatx", false, "create a share for a data transfer") + + rol := cmd.String("rol", "viewer", "the permission for the share (viewer or editor) / applies to webdav and webapp") cmd.ResetFlags = func() { - *grantType, *grantee, *idp, *rol, *userType = "user", "", "", "viewer", "primary" + *grantType, *grantee, *idp, *rol, *userType, *webdav, *webapp, *datatx = "user", "", "", "viewer", "primary", false, false, false } cmd.Action = func(w ...io.Writer) error { - if cmd.NArg() < 1 { - return errors.New("Invalid arguments: " + cmd.Usage()) - } - // validate flags if *grantee == "" { return errors.New("Grantee cannot be empty: use -grantee flag\n" + cmd.Usage()) } - if *idp == "" { return errors.New("IdP cannot be empty: use -idp flag\n" + cmd.Usage()) } + if *spaceID == "" { + return errors.New("SpaceID cannot be empty: use -spaceid flag\n" + cmd.Usage()) + } + if *opaqueID == "" { + return errors.New("Path cannot be empty: use -path flag\n" + cmd.Usage()) + } - fn := cmd.Args()[0] + if !*webdav && !*webapp && !*datatx { + return errors.New("No access method chosen. Available methods: webdav, webapp, datatx\n" + cmd.Usage()) + } ctx := getAuthContext() client, err := getClient() @@ -94,7 +101,12 @@ func ocmShareCreateCommand() *command { return formatError(remoteUserRes.Status) } - ref := &provider.Reference{Path: fn} + ref := &provider.Reference{ + ResourceId: &provider.ResourceId{ + SpaceId: *spaceID, + OpaqueId: *opaqueID, + }, + } req := &provider.StatRequest{Ref: ref} res, err := client.Stat(ctx, req) if err != nil { @@ -104,40 +116,22 @@ func ocmShareCreateCommand() *command { return formatError(res.Status) } - perm, pint, err := getOCMSharePerm(*rol) + gt := getGrantType(*grantType) + am, err := getAccessMethods(*webdav, *webapp, *datatx, *rol) if err != nil { return err } - gt := getGrantType(*grantType) - grant := &ocm.ShareGrant{ - Permissions: perm, + shareRequest := &ocm.CreateOCMShareRequest{ + ResourceId: res.Info.Id, Grantee: &provider.Grantee{ Type: gt, // For now, we only support user shares. // TODO (ishank011): To be updated once this is decided. - Id: &provider.Grantee_UserId{UserId: u}, - }, - } - - opaqueObj := &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "plain", - Value: []byte(strconv.Itoa(pint)), - }, - "name": { - Decoder: "plain", - Value: []byte(res.Info.Path), - }, + Id: &provider.Grantee_UserId{UserId: remoteUserRes.RemoteUser.Id}, }, - } - - shareRequest := &ocm.CreateOCMShareRequest{ - Opaque: opaqueObj, - ResourceId: res.Info.Id, - Grant: grant, RecipientMeshProvider: providerInfo.ProviderInfo, + AccessMethods: am, } shareRes, err := client.CreateOCMShare(ctx, shareRequest) @@ -151,11 +145,12 @@ func ocmShareCreateCommand() *command { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated"}) + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Type", "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated"}) + // TODO (gdelmont): expose protocols info s := shareRes.Share t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, }) @@ -166,15 +161,44 @@ func ocmShareCreateCommand() *command { return cmd } -func getOCMSharePerm(p string) (*ocm.SharePermissions, int, error) { - if p == viewerPermission { - return &ocm.SharePermissions{ - Permissions: conversions.NewViewerRole(_resharing).CS3ResourcePermissions(), - }, 1, nil - } else if p == editorPermission { - return &ocm.SharePermissions{ - Permissions: conversions.NewEditorRole(_resharing).CS3ResourcePermissions(), - }, 15, nil +func getAccessMethods(webdav, webapp, datatx bool, rol string) ([]*ocm.AccessMethod, error) { + var m []*ocm.AccessMethod + if webdav { + perm, err := getOCMSharePerm(rol) + if err != nil { + return nil, err + } + m = append(m, ocmshare.NewWebDavAccessMethod(perm)) + } + if webapp { + v, err := getOCMViewMode(rol) + if err != nil { + return nil, err + } + m = append(m, ocmshare.NewWebappAccessMethod(v)) + } + if datatx { + m = append(m, ocmshare.NewTransferAccessMethod()) + } + return m, nil +} + +func getOCMSharePerm(p string) (*provider.ResourcePermissions, error) { + switch p { + case viewerPermission: + return conversions.NewViewerRole(false).CS3ResourcePermissions(), nil + case editorPermission: + return conversions.NewEditorRole(false).CS3ResourcePermissions(), nil + } + return nil, errors.New("invalid rol: " + p) +} + +func getOCMViewMode(p string) (appprovider.ViewMode, error) { + switch p { + case viewerPermission: + return appprovider.ViewMode_VIEW_MODE_READ_ONLY, nil + case editorPermission: + return appprovider.ViewMode_VIEW_MODE_READ_WRITE, nil } - return nil, 0, errors.New("invalid rol: " + p) + return 0, errors.New("invalid rol: " + p) } diff --git a/cmd/reva/ocm-share-get-received.go b/cmd/reva/ocm-share-get-received.go new file mode 100644 index 0000000000..1aac47a755 --- /dev/null +++ b/cmd/reva/ocm-share-get-received.go @@ -0,0 +1,74 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package main + +import ( + "errors" + "fmt" + "io" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + "github.com/cs3org/reva/v2/pkg/utils" +) + +func ocmShareGetReceivedCommand() *command { + cmd := newCommand("ocm-share-get-received") + cmd.Description = func() string { return "get the info of the OCM share you have received" } + cmd.Usage = func() string { return "Usage: ocm-share-get-received" } + cmd.Action = func(w ...io.Writer) error { + if cmd.NArg() < 1 { + return errors.New("Invalid arguments: " + cmd.Usage()) + } + + ctx := getAuthContext() + client, err := getClient() + if err != nil { + return err + } + + id := cmd.Arg(0) + + shareRes, err := client.GetReceivedOCMShare(ctx, &ocm.GetReceivedOCMShareRequest{ + Ref: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: id, + }, + }, + }, + }) + if err != nil { + return err + } + if shareRes.Status.Code != rpc.Code_CODE_OK { + return formatError(shareRes.Status) + } + + d, err := utils.MarshalProtoV1ToJSON(shareRes.Share) + if err != nil { + return err + } + + fmt.Println(string(d)) + + return nil + } + return cmd +} diff --git a/cmd/reva/ocm-share-get.go b/cmd/reva/ocm-share-get.go new file mode 100644 index 0000000000..5bd86c3eca --- /dev/null +++ b/cmd/reva/ocm-share-get.go @@ -0,0 +1,74 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package main + +import ( + "errors" + "fmt" + "io" + + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + "github.com/cs3org/reva/v2/pkg/utils" +) + +func ocmShareGetCommand() *command { + cmd := newCommand("ocm-share-get") + cmd.Description = func() string { return "get the info of the OCM share" } + cmd.Usage = func() string { return "Usage: ocm-share-get" } + cmd.Action = func(w ...io.Writer) error { + if cmd.NArg() < 1 { + return errors.New("Invalid arguments: " + cmd.Usage()) + } + + ctx := getAuthContext() + client, err := getClient() + if err != nil { + return err + } + + id := cmd.Arg(0) + + shareRes, err := client.GetOCMShare(ctx, &ocm.GetOCMShareRequest{ + Ref: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: &ocm.ShareId{ + OpaqueId: id, + }, + }, + }, + }) + if err != nil { + return err + } + if shareRes.Status.Code != rpc.Code_CODE_OK { + return formatError(shareRes.Status) + } + + d, err := utils.MarshalProtoV1ToJSON(shareRes.Share) + if err != nil { + return err + } + + fmt.Println(string(d)) + + return nil + } + return cmd +} diff --git a/cmd/reva/ocm-share-list-received.go b/cmd/reva/ocm-share-list-received.go index 851264c7b9..f6e9971c26 100644 --- a/cmd/reva/ocm-share-list-received.go +++ b/cmd/reva/ocm-share-list-received.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -54,14 +54,14 @@ func ocmShareListReceivedCommand() *command { if len(w) == 0 { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "RemoteShareId", "Type", "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated", "State", "ShareType"}) for _, s := range shareRes.Shares { t.AppendRows([]table.Row{ - {s.Share.Id.OpaqueId, s.Share.Owner.Idp, s.Share.Owner.OpaqueId, s.Share.ResourceId.String(), - s.Share.Permissions.String(), s.Share.Grantee.Type.String(), s.Share.Grantee.GetUserId().Idp, - s.Share.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Share.Ctime.Seconds), 0), - time.Unix(int64(s.Share.Mtime.Seconds), 0), s.State.String(), s.Share.ShareType.String()}, + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.RemoteShareId, + s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, + s.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Ctime.Seconds), 0), + time.Unix(int64(s.Mtime.Seconds), 0), s.State.String(), s.ShareType.String()}, }) } t.Render() diff --git a/cmd/reva/ocm-share-list.go b/cmd/reva/ocm-share-list.go index 185a15d398..06f2e60cbd 100644 --- a/cmd/reva/ocm-share-list.go +++ b/cmd/reva/ocm-share-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -76,12 +76,12 @@ func ocmShareListCommand() *command { if len(w) == 0 { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Type", "ShareType", "Grantee.Idp", "Grantee.OpaqueId", "Created", "Updated"}) for _, s := range shareRes.Shares { t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Grantee.Type.String(), s.ShareType.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, }) diff --git a/cmd/reva/ocm-share-remove.go b/cmd/reva/ocm-share-remove.go index 8332c77407..25b3a4c730 100644 --- a/cmd/reva/ocm-share-remove.go +++ b/cmd/reva/ocm-share-remove.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/ocm-share-update-received.go b/cmd/reva/ocm-share-update-received.go index 7f88765f14..92b5b2d80c 100644 --- a/cmd/reva/ocm-share-update-received.go +++ b/cmd/reva/ocm-share-update-received.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,6 +24,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/pkg/errors" "google.golang.org/protobuf/types/known/fieldmaskpb" ) @@ -33,9 +34,11 @@ func ocmShareUpdateReceivedCommand() *command { cmd.Description = func() string { return "update a received OCM share" } cmd.Usage = func() string { return "Usage: ocm-share-update-received [-flags] " } state := cmd.String("state", "pending", "the state of the share (pending, accepted or rejected)") + path := cmd.String("path", "", "the destination path of the data transfer (ignored if this is not a transfer type share)") cmd.ResetFlags = func() { *state = "pending" + *path = "" } cmd.Action = func(w ...io.Writer) error { @@ -75,9 +78,25 @@ func ocmShareUpdateReceivedCommand() *command { } shareRes.Share.State = shareState + // check if we are dealing with a transfer in case the destination path needs to be set + _, ok := getTransferProtocol(shareRes.Share) + var opaque *typesv1beta1.Opaque + if ok { + // transfer_destination_path is not part of TransferProtocol and is specified as an opaque field + opaque = &typesv1beta1.Opaque{ + Map: map[string]*typesv1beta1.OpaqueEntry{ + "transfer_destination_path": { + Decoder: "plain", + Value: []byte(*path), + }, + }, + } + } + shareRequest := &ocm.UpdateReceivedOCMShareRequest{ Share: shareRes.Share, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{"state"}}, + Opaque: opaque, } updateRes, err := shareClient.UpdateReceivedOCMShare(ctx, shareRequest) @@ -95,6 +114,15 @@ func ocmShareUpdateReceivedCommand() *command { return cmd } +func getTransferProtocol(share *ocm.ReceivedShare) (*ocm.TransferProtocol, bool) { + for _, p := range share.Protocols { + if d, ok := p.Term.(*ocm.Protocol_TransferOptions); ok { + return d.TransferOptions, true + } + } + return nil, false +} + func getOCMShareState(state string) ocm.ShareState { switch state { case "pending": diff --git a/cmd/reva/ocm-share-update.go b/cmd/reva/ocm-share-update.go index e77389f411..c1985a91b3 100644 --- a/cmd/reva/ocm-share-update.go +++ b/cmd/reva/ocm-share-update.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,30 +31,26 @@ func ocmShareUpdateCommand() *command { cmd := newCommand("ocm-share-update") cmd.Description = func() string { return "update an OCM share" } cmd.Usage = func() string { return "Usage: ocm-share-update [-flags] " } - rol := cmd.String("rol", "viewer", "the permission for the share (viewer or editor)") + + webdavRol := cmd.String("webdav-rol", "viewer", "the permission for the WebDAV access method (viewer or editor)") + webappViewMode := cmd.String("webapp-mode", "view", "the view mode for the Webapp access method (read or write)") cmd.ResetFlags = func() { - *rol = "viewer" + *webdavRol, *webappViewMode = "viewer", "read" } cmd.Action = func(w ...io.Writer) error { if cmd.NArg() < 1 { return errors.New("Invalid arguments: " + cmd.Usage()) } - // validate flags - if *rol != viewerPermission && *rol != editorPermission { - return errors.New("Invalid rol: rol must be viewer or editor\n" + cmd.Usage()) - } - id := cmd.Args()[0] - ctx := getAuthContext() - shareClient, err := getClient() - if err != nil { - return err + if *webdavRol == "" && *webappViewMode == "" { + return errors.New("use at least one of -webdav-rol or -webapp-mode flag") } - perm, _, err := getOCMSharePerm(*rol) + ctx := getAuthContext() + shareClient, err := getClient() if err != nil { return err } @@ -67,11 +63,42 @@ func ocmShareUpdateCommand() *command { }, }, }, - Field: &ocm.UpdateOCMShareRequest_UpdateField{ - Field: &ocm.UpdateOCMShareRequest_UpdateField_Permissions{ - Permissions: perm, + } + + if *webdavRol != "" { + perm, err := getOCMSharePerm(*webdavRol) + if err != nil { + return err + } + shareRequest.Field = append(shareRequest.Field, &ocm.UpdateOCMShareRequest_UpdateField{ + Field: &ocm.UpdateOCMShareRequest_UpdateField_AccessMethods{ + AccessMethods: &ocm.AccessMethod{ + Term: &ocm.AccessMethod_WebdavOptions{ + WebdavOptions: &ocm.WebDAVAccessMethod{ + Permissions: perm, + }, + }, + }, }, - }, + }) + } + + if *webappViewMode != "" { + mode, err := getOCMViewMode(*webappViewMode) + if err != nil { + return err + } + shareRequest.Field = append(shareRequest.Field, &ocm.UpdateOCMShareRequest_UpdateField{ + Field: &ocm.UpdateOCMShareRequest_UpdateField_AccessMethods{ + AccessMethods: &ocm.AccessMethod{ + Term: &ocm.AccessMethod_WebappOptions{ + WebappOptions: &ocm.WebappAccessMethod{ + ViewMode: mode, + }, + }, + }, + }, + }) } shareRes, err := shareClient.UpdateOCMShare(ctx, shareRequest) diff --git a/cmd/reva/open-in-app.go b/cmd/reva/open-in-app.go index a831c676ae..510c63c538 100644 --- a/cmd/reva/open-in-app.go +++ b/cmd/reva/open-in-app.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/preferences.go b/cmd/reva/preferences.go index 51471950bd..ae7647d04a 100644 --- a/cmd/reva/preferences.go +++ b/cmd/reva/preferences.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/public-share-create.go b/cmd/reva/public-share-create.go index f22eac189b..9b2b3ebae5 100644 --- a/cmd/reva/public-share-create.go +++ b/cmd/reva/public-share-create.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -35,9 +35,11 @@ func publicShareCreateCommand() *command { cmd.Description = func() string { return "create a public share" } cmd.Usage = func() string { return "Usage: public-share-create [-flags] " } rol := cmd.String("rol", "viewer", "the permission for the share (viewer or editor)") + description := cmd.String("description", "", "the description for the share") + internal := cmd.Bool("internal", false, "mark the public share as internal") cmd.ResetFlags = func() { - *rol = "viewer" + *rol, *description, *internal = "viewer", "", false } cmd.Action = func(w ...io.Writer) error { @@ -78,6 +80,8 @@ func publicShareCreateCommand() *command { shareRequest := &link.CreatePublicShareRequest{ ResourceInfo: res.Info, Grant: grant, + Description: *description, + Internal: *internal, } shareRes, err := client.CreatePublicShare(ctx, shareRequest) @@ -91,11 +95,11 @@ func publicShareCreateCommand() *command { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Token", "Expiration", "Created", "Updated"}) + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Token", "Expiration", "Created", "Updated", "Description"}) s := shareRes.Share t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), s.Token, s.Expiration.String(), time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), s.Token, s.Expiration.String(), time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0), s.Description}, }) t.Render() diff --git a/cmd/reva/public-share-list.go b/cmd/reva/public-share-list.go index b21a15c18b..edc41689b2 100644 --- a/cmd/reva/public-share-list.go +++ b/cmd/reva/public-share-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -76,11 +76,11 @@ func publicShareListCommand() *command { if len(w) == 0 { t := table.NewWriter() t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Token", "Expiration", "Created", "Updated"}) + t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Token", "Expiration", "Created", "Updated", "Description"}) for _, s := range shareRes.Share { t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), s.Token, s.Expiration.String(), time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, + {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), s.Token, s.Expiration.String(), time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0), s.Description}, }) } t.Render() diff --git a/cmd/reva/public-share-remove.go b/cmd/reva/public-share-remove.go index 73ffc374fe..459199845d 100644 --- a/cmd/reva/public-share-remove.go +++ b/cmd/reva/public-share-remove.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/public-share-update.go b/cmd/reva/public-share-update.go index 1e55cb46bc..05a1a21d4a 100644 --- a/cmd/reva/public-share-update.go +++ b/cmd/reva/public-share-update.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,9 +32,10 @@ func publicShareUpdateCommand() *command { cmd.Description = func() string { return "update a public share" } cmd.Usage = func() string { return "Usage: public-share-update [-flags] " } rol := cmd.String("rol", "viewer", "the permission for the share (viewer or editor)") + description := cmd.String("description", "", "the description for the share") cmd.ResetFlags = func() { - *rol = "viewer" + *rol, *description = "viewer", "" } cmd.Action = func(w ...io.Writer) error { if cmd.NArg() < 1 { @@ -59,7 +60,9 @@ func publicShareUpdateCommand() *command { return err } - shareRequest := &link.UpdatePublicShareRequest{ + var updates []*link.UpdatePublicShareRequest + + updates = append(updates, &link.UpdatePublicShareRequest{ Ref: &link.PublicShareReference{ Spec: &link.PublicShareReference_Id{ Id: &link.PublicShareId{ @@ -75,15 +78,33 @@ func publicShareUpdateCommand() *command { }, }, }, - } + }) - shareRes, err := shareClient.UpdatePublicShare(ctx, shareRequest) - if err != nil { - return err + if *description != "" { + updates = append(updates, &link.UpdatePublicShareRequest{ + Ref: &link.PublicShareReference{ + Spec: &link.PublicShareReference_Id{ + Id: &link.PublicShareId{ + OpaqueId: id, + }, + }, + }, + Update: &link.UpdatePublicShareRequest_Update{ + Type: link.UpdatePublicShareRequest_Update_TYPE_DESCRIPTION, + Description: *description, + }, + }) } - if shareRes.Status.Code != rpc.Code_CODE_OK { - return formatError(shareRes.Status) + for _, u := range updates { + shareRes, err := shareClient.UpdatePublicShare(ctx, u) + if err != nil { + return err + } + + if shareRes.Status.Code != rpc.Code_CODE_OK { + return formatError(shareRes.Status) + } } fmt.Println("OK") diff --git a/cmd/reva/recycle-list.go b/cmd/reva/recycle-list.go index d94f9bc119..af5a1cf3e8 100644 --- a/cmd/reva/recycle-list.go +++ b/cmd/reva/recycle-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/recycle-purge.go b/cmd/reva/recycle-purge.go index e385636049..3120bc6be7 100644 --- a/cmd/reva/recycle-purge.go +++ b/cmd/reva/recycle-purge.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/recycle-restore.go b/cmd/reva/recycle-restore.go index a081251053..a644943796 100644 --- a/cmd/reva/recycle-restore.go +++ b/cmd/reva/recycle-restore.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/rm.go b/cmd/reva/rm.go index d992564528..60cb4f423b 100644 --- a/cmd/reva/rm.go +++ b/cmd/reva/rm.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/setlock.go b/cmd/reva/setlock.go index da85f47d4f..c1cd09a111 100644 --- a/cmd/reva/setlock.go +++ b/cmd/reva/setlock.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/share-create.go b/cmd/reva/share-create.go index f31013a614..1806cbeeaf 100644 --- a/cmd/reva/share-create.go +++ b/cmd/reva/share-create.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -159,11 +159,11 @@ func getGrantType(t string) provider.GranteeType { func getSharePerm(p string) (*provider.ResourcePermissions, error) { switch p { case viewerPermission: - return conversions.NewViewerRole(_resharing).CS3ResourcePermissions(), nil + return conversions.NewViewerRole(false).CS3ResourcePermissions(), nil case editorPermission: - return conversions.NewEditorRole(_resharing).CS3ResourcePermissions(), nil + return conversions.NewEditorRole(false).CS3ResourcePermissions(), nil case collabPermission: - return conversions.NewManagerRole().CS3ResourcePermissions(), nil + return conversions.NewCoownerRole().CS3ResourcePermissions(), nil case denyPermission: return &provider.ResourcePermissions{}, nil default: diff --git a/cmd/reva/share-list-received.go b/cmd/reva/share-list-received.go index 97c7d82b19..f877728d1a 100644 --- a/cmd/reva/share-list-received.go +++ b/cmd/reva/share-list-received.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/share-list.go b/cmd/reva/share-list.go index 6f5f326399..d3ba5a265b 100644 --- a/cmd/reva/share-list.go +++ b/cmd/reva/share-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/share-remove.go b/cmd/reva/share-remove.go index ae24282230..fbea4436cf 100644 --- a/cmd/reva/share-remove.go +++ b/cmd/reva/share-remove.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/share-update-received.go b/cmd/reva/share-update-received.go index 624fe3e983..1f7b1f0818 100644 --- a/cmd/reva/share-update-received.go +++ b/cmd/reva/share-update-received.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/share-update.go b/cmd/reva/share-update.go index 00603cd994..2d4eba83d4 100644 --- a/cmd/reva/share-update.go +++ b/cmd/reva/share-update.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/stat.go b/cmd/reva/stat.go index c84cba9a0f..ac987b2c65 100644 --- a/cmd/reva/stat.go +++ b/cmd/reva/stat.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/transfer-cancel.go b/cmd/reva/transfer-cancel.go index dfa086467b..2ebf6353a8 100644 --- a/cmd/reva/transfer-cancel.go +++ b/cmd/reva/transfer-cancel.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/transfer-create.go b/cmd/reva/transfer-create.go deleted file mode 100644 index 7902a3281a..0000000000 --- a/cmd/reva/transfer-create.go +++ /dev/null @@ -1,168 +0,0 @@ -// 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 main - -import ( - "io" - "os" - "strconv" - "strings" - "time" - - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - 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" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/v2/pkg/utils" - "github.com/jedib0t/go-pretty/table" - "github.com/pkg/errors" -) - -func transferCreateCommand() *command { - cmd := newCommand("transfer-create") - cmd.Description = func() string { return "create transfer between 2 sites" } - cmd.Usage = func() string { return "Usage: transfer-create [-flags] " } - grantee := cmd.String("grantee", "", "the grantee, receiver of the transfer") - granteeType := cmd.String("granteeType", "user", "the grantee type, one of: user, group (defaults to user)") - idp := cmd.String("idp", "", "the idp of the grantee") - userType := cmd.String("user-type", "primary", "the type of user account, defaults to primary") - - cmd.Action = func(w ...io.Writer) error { - if cmd.NArg() < 1 { - return errors.New("Invalid arguments: " + cmd.Usage()) - } - - if *grantee == "" { - return errors.New("Grantee cannot be empty: use -grantee flag\n" + cmd.Usage()) - } - if *idp == "" { - return errors.New("Idp cannot be empty: use -idp flag\n" + cmd.Usage()) - } - - // the resource to transfer; the path - fn := cmd.Args()[0] - - ctx := getAuthContext() - client, err := getClient() - if err != nil { - return err - } - - u := &userpb.UserId{OpaqueId: *grantee, Idp: *idp, Type: utils.UserTypeMap(*userType)} - - // check if invitation has been accepted - acceptedUserRes, err := client.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ - RemoteUserId: u, - }) - if err != nil { - return err - } - if acceptedUserRes.Status.Code != rpc.Code_CODE_OK { - return formatError(acceptedUserRes.Status) - } - - // verify resource stats - statReq := &provider.StatRequest{ - Ref: &provider.Reference{Path: fn}, - } - statRes, err := client.Stat(ctx, statReq) - if err != nil { - return err - } - if statRes.Status.Code != rpc.Code_CODE_OK { - return formatError(statRes.Status) - } - - providerInfoResp, err := client.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ - Domain: *idp, - }) - if err != nil { - return err - } - - resourcePermissions, pint, err := getOCMSharePerm(editorPermission) - if err != nil { - return err - } - - gt := provider.GranteeType_GRANTEE_TYPE_USER - if strings.ToLower(*granteeType) == "group" { - gt = provider.GranteeType_GRANTEE_TYPE_GROUP - } - - createShareReq := &ocm.CreateOCMShareRequest{ - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "plain", - Value: []byte(strconv.Itoa(pint)), - }, - "name": { - Decoder: "plain", - Value: []byte(statRes.Info.Path), - }, - "protocol": { - Decoder: "plain", - Value: []byte("datatx"), - }, - }, - }, - ResourceId: statRes.Info.Id, - Grant: &ocm.ShareGrant{ - Grantee: &provider.Grantee{ - Type: gt, - Id: &provider.Grantee_UserId{ - UserId: u, - }, - }, - Permissions: resourcePermissions, - }, - RecipientMeshProvider: providerInfoResp.ProviderInfo, - } - - createShareResponse, err := client.CreateOCMShare(ctx, createShareReq) - if err != nil { - return err - } - if createShareResponse.Status.Code != rpc.Code_CODE_OK { - if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - return formatError(statRes.Status) - } - return err - } - - t := table.NewWriter() - t.SetOutputMirror(os.Stdout) - t.AppendHeader(table.Row{"#", "Owner.Idp", "Owner.OpaqueId", "ResourceId", "Permissions", "Type", "Grantee.Idp", "Grantee.OpaqueId", "ShareType", "Created", "Updated"}) - - s := createShareResponse.Share - t.AppendRows([]table.Row{ - {s.Id.OpaqueId, s.Owner.Idp, s.Owner.OpaqueId, s.ResourceId.String(), s.Permissions.String(), - s.Grantee.Type.String(), s.Grantee.GetUserId().Idp, s.Grantee.GetUserId().OpaqueId, s.ShareType.String(), - time.Unix(int64(s.Ctime.Seconds), 0), time.Unix(int64(s.Mtime.Seconds), 0)}, - }) - t.Render() - return nil - } - - return cmd -} diff --git a/cmd/reva/transfer-get-status.go b/cmd/reva/transfer-get-status.go index ab73a52e69..7fa0b0fe76 100644 --- a/cmd/reva/transfer-get-status.go +++ b/cmd/reva/transfer-get-status.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/transfer-list.go b/cmd/reva/transfer-list.go index b0ea83ae9d..526e4c7e7c 100644 --- a/cmd/reva/transfer-list.go +++ b/cmd/reva/transfer-list.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/transfer-retry.go b/cmd/reva/transfer-retry.go index 2f2ada08d9..9860b46cf2 100644 --- a/cmd/reva/transfer-retry.go +++ b/cmd/reva/transfer-retry.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/unlock.go b/cmd/reva/unlock.go index 567c0a26d5..0cd189bffc 100644 --- a/cmd/reva/unlock.go +++ b/cmd/reva/unlock.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/upload.go b/cmd/reva/upload.go index a2c4d4efd6..f0161741de 100644 --- a/cmd/reva/upload.go +++ b/cmd/reva/upload.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -27,24 +27,23 @@ import ( "path/filepath" "strconv" - "github.com/cs3org/reva/v2/internal/http/services/datagateway" - "github.com/pkg/errors" - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/cs3org/reva/v2/pkg/errtypes" - "github.com/eventials/go-tus" - "github.com/eventials/go-tus/memorystore" - "github.com/studio-b12/gowebdav" // TODO(labkode): this should not come from this package. "github.com/cs3org/reva/v2/internal/grpc/services/storageprovider" + "github.com/cs3org/reva/v2/internal/http/services/datagateway" "github.com/cs3org/reva/v2/pkg/crypto" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/rhttp" "github.com/cs3org/reva/v2/pkg/utils" + "github.com/eventials/go-tus" + "github.com/eventials/go-tus/memorystore" + "github.com/pkg/errors" + "github.com/studio-b12/gowebdav" ) func uploadCommand() *command { @@ -148,7 +147,7 @@ func uploadCommand() *command { dataServerURL := p.UploadEndpoint if *protocolFlag == "simple" { - httpReq, err := rhttp.NewRequest(ctx, "PUT", dataServerURL, fd) + httpReq, err := rhttp.NewRequest(ctx, http.MethodPut, dataServerURL, fd) if err != nil { return err } @@ -218,7 +217,7 @@ func uploadCommand() *command { info := res2.Info - fmt.Printf("File uploaded: %s %d %s\n", info.Id, info.Size, info.Path) + fmt.Printf("File uploaded: %s:%s %d %s\n", info.Id.StorageId, info.Id.OpaqueId, info.Size, info.Path) return nil } @@ -292,7 +291,6 @@ func computeXS(t provider.ResourceChecksumType, r io.Reader) (string, error) { return "", nil default: return "", fmt.Errorf("invalid checksum: %s", t) - } } diff --git a/cmd/reva/version.go b/cmd/reva/version.go index ad8d725088..578e7dc68e 100644 --- a/cmd/reva/version.go +++ b/cmd/reva/version.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/reva/whoami.go b/cmd/reva/whoami.go index acd2c4c526..5ed676f9a5 100644 --- a/cmd/reva/whoami.go +++ b/cmd/reva/whoami.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/cmd/revad/runtime/loader.go b/cmd/revad/runtime/loader.go index 486a00518c..658ed85223 100644 --- a/cmd/revad/runtime/loader.go +++ b/cmd/revad/runtime/loader.go @@ -36,9 +36,9 @@ import ( _ "github.com/cs3org/reva/v2/pkg/datatx/manager/loader" _ "github.com/cs3org/reva/v2/pkg/group/manager/loader" _ "github.com/cs3org/reva/v2/pkg/metrics/driver/loader" - _ "github.com/cs3org/reva/v2/pkg/ocm/invite/manager/loader" + _ "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/loader" _ "github.com/cs3org/reva/v2/pkg/ocm/provider/authorizer/loader" - _ "github.com/cs3org/reva/v2/pkg/ocm/share/manager/loader" + _ "github.com/cs3org/reva/v2/pkg/ocm/share/repository/loader" _ "github.com/cs3org/reva/v2/pkg/permission/manager/loader" _ "github.com/cs3org/reva/v2/pkg/preferences/loader" _ "github.com/cs3org/reva/v2/pkg/publicshare/manager/loader" diff --git a/go.mod b/go.mod index 0eac55bb18..df5fdfc6c2 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( 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-20230516150832-730ac860c71d + github.com/cs3org/go-cs3apis v0.0.0-20230727093620-0f4399be4543 github.com/dgraph-io/ristretto v0.1.1 github.com/emvi/iso-639-1 v1.0.1 github.com/eventials/go-tus v0.0.0-20220610120217-05d0564bb571 @@ -29,6 +29,9 @@ require ( github.com/go-micro/plugins/v4/server/http v1.2.1 github.com/go-micro/plugins/v4/store/nats-js v1.1.0 github.com/go-micro/plugins/v4/store/redis v1.2.1-0.20230510195111-07cd57e1bc9d + github.com/go-playground/locales v0.13.0 + github.com/go-playground/universal-translator v0.17.0 + github.com/go-playground/validator/v10 v10.4.1 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.6.0 github.com/gofrs/flock v0.8.1 @@ -54,6 +57,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/nats-io/nats-server/v2 v2.9.19 github.com/nats-io/nats.go v1.27.0 + github.com/onsi/ginkgo v1.16.5 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.8 github.com/owncloud/ocis/v2 v2.0.1-0.20230606150602-25d7dae4667b @@ -64,7 +68,6 @@ require ( github.com/rogpeppe/go-internal v1.10.0 github.com/rs/cors v1.9.0 github.com/rs/zerolog v1.29.1 - github.com/sciencemesh/meshdirectory-web v1.0.4 github.com/sethvargo/go-password v0.2.0 github.com/shamaton/msgpack/v2 v2.1.1 github.com/stretchr/testify v1.8.4 @@ -83,7 +86,7 @@ require ( go.opentelemetry.io/otel/sdk v1.16.0 go.opentelemetry.io/otel/trace v1.16.0 golang.org/x/crypto v0.11.0 - golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6 + golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 golang.org/x/oauth2 v0.10.0 golang.org/x/sync v0.2.0 golang.org/x/sys v0.10.0 @@ -155,6 +158,7 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect + github.com/leodido/go-urn v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.17 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect @@ -224,4 +228,4 @@ require ( ) // the replacement build is based on https://github.com/dragonchaser/cs3apis/tree/master -replace github.com/cs3org/go-cs3apis => github.com/dragonchaser/go-cs3apis v0.0.0-20230918130959-ae732d4b8147 +replace github.com/cs3org/go-cs3apis => github.com/aduffeck/go-cs3apis v0.0.0-20231009082215-ad45e19edac0 diff --git a/go.sum b/go.sum index d6ad28dcea..fa52827689 100644 --- a/go.sum +++ b/go.sum @@ -424,6 +424,8 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/aduffeck/go-cs3apis v0.0.0-20231009082215-ad45e19edac0 h1:y9gvblZJHMENTzgolQ9sChKONSILkJ4U/j7OqMIF4QM= +github.com/aduffeck/go-cs3apis v0.0.0-20231009082215-ad45e19edac0/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -529,8 +531,6 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/dragonchaser/go-cs3apis v0.0.0-20230918130959-ae732d4b8147 h1:DIEKyIGBNDDzdwnHlbX2BFABHol6c9EEHuCDdVKgWsA= -github.com/dragonchaser/go-cs3apis v0.0.0-20230918130959-ae732d4b8147/go.mod h1:TKUgPjk4kNU0KLd9ZxoFE74Wv6PiXckF4RB3749FztA= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= @@ -644,9 +644,13 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/validate v0.21.0/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= @@ -654,6 +658,7 @@ github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfC github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.1/go.mod h1:dcoOX6HbPZSZptuspn9bctJ+N/CnF5gGygcUP3XYfe4= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0= @@ -933,6 +938,7 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= @@ -1031,6 +1037,7 @@ github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= @@ -1039,10 +1046,14 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.11.0 h1:WgqUCUt/lT6yXoQ8Wef0fsNn5cAuMK7+KT9UFRz2tcU= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.27.8 h1:gegWiwZjBsf2DgiSbf5hpokZ98JVDMcWkUiigk6/KXc= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1134,8 +1145,6 @@ github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= -github.com/sciencemesh/meshdirectory-web v1.0.4 h1:1YSctF6PAXhoHUYCaeRTj7rHaF7b3rYrZf2R0VXBIbo= -github.com/sciencemesh/meshdirectory-web v1.0.4/go.mod h1:fJSThTS3xf+sTdL0iXQoaQJssLI7tn7DetHMHUl4SRk= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUtVbo7ada43DJhG55ua/hjS5I= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -1305,8 +1314,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6 h1:mC6uOkPi9SUk8A59jZvw7//rlyc+MlELtQUCyOUSKZQ= -golang.org/x/exp v0.0.0-20221026004748-78e5e7837ae6/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= +golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1364,6 +1373,7 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -1468,6 +1478,7 @@ golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1504,6 +1515,7 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1643,6 +1655,7 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210112230658-8b4aab62c064/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1729,6 +1742,7 @@ google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6 google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190404172233-64821d5d2107/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= diff --git a/internal/grpc/interceptors/auth/scope.go b/internal/grpc/interceptors/auth/scope.go index 44c2e6d5b2..1b5edd9f20 100644 --- a/internal/grpc/interceptors/auth/scope.go +++ b/internal/grpc/interceptors/auth/scope.go @@ -32,6 +32,7 @@ import ( rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" registry "github.com/cs3org/go-cs3apis/cs3/storage/registry/v1beta1" "github.com/cs3org/reva/v2/pkg/appctx" @@ -83,6 +84,10 @@ func expandAndVerifyScope(ctx context.Context, req interface{}, tokenScope map[s if err = resolveLightweightScope(ctx, ref, tokenScope[k], user, client, mgr); err == nil { return nil } + case strings.HasPrefix(k, "ocmshare"): + if err = resolveOCMShare(ctx, ref, tokenScope[k], client, mgr); err == nil { + return nil + } } log.Err(err).Msgf("error resolving reference %s under scope %+v", ref.String(), k) } @@ -168,6 +173,21 @@ func resolvePublicShare(ctx context.Context, ref *provider.Reference, scope *aut return checkRelativeReference(ctx, ref, share.ResourceId, client) } +func resolveOCMShare(ctx context.Context, ref *provider.Reference, scope *authpb.Scope, client gateway.GatewayAPIClient, mgr token.Manager) error { + var share ocmv1beta1.Share + if err := utils.UnmarshalJSONToProtoV1(scope.Resource.Value, &share); err != nil { + return err + } + + if err := checkCacheForNestedResource(ctx, ref, share.ResourceId, client, mgr); err == nil { + return nil + } + + // Some services like wopi don't access the shared resource relative to the + // share root but instead relative to the shared resources parent. + return checkRelativeReference(ctx, ref, share.ResourceId, client) +} + // checkRelativeReference checks if the shared resource is being accessed via a relative reference // e.g.: // storage: abcd, space: efgh diff --git a/internal/grpc/services/gateway/appprovider.go b/internal/grpc/services/gateway/appprovider.go index 8119118d78..3a91e12a0d 100644 --- a/internal/grpc/services/gateway/appprovider.go +++ b/internal/grpc/services/gateway/appprovider.go @@ -171,7 +171,7 @@ func (s *svc) openLocalResources(ctx context.Context, ri *storageprovider.Resour appProviderReq := &providerpb.OpenInAppRequest{ ResourceInfo: ri, - ViewMode: providerpb.OpenInAppRequest_ViewMode(vm), + ViewMode: providerpb.ViewMode(vm), AccessToken: accessToken, Opaque: opaque, } diff --git a/internal/grpc/services/gateway/ocmcore.go b/internal/grpc/services/gateway/ocmcore.go index 9d47f28098..8c47bdc76b 100644 --- a/internal/grpc/services/gateway/ocmcore.go +++ b/internal/grpc/services/gateway/ocmcore.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -42,3 +42,35 @@ func (s *svc) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCore return res, nil } + +func (s *svc) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) { + c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint) + if err != nil { + return &ocmcore.UpdateOCMCoreShareResponse{ + Status: status.NewInternal(ctx, "error getting ocm core client"), + }, nil + } + + res, err := c.UpdateOCMCoreShare(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare") + } + + return res, nil +} + +func (s *svc) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) { + c, err := pool.GetOCMCoreClient(s.c.OCMCoreEndpoint) + if err != nil { + return &ocmcore.DeleteOCMCoreShareResponse{ + Status: status.NewInternal(ctx, "error getting ocm core client"), + }, nil + } + + res, err := c.DeleteOCMCoreShare(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling UpdateOCMCoreShare") + } + + return res, nil +} diff --git a/internal/grpc/services/gateway/ocminvitemanager.go b/internal/grpc/services/gateway/ocminvitemanager.go index bbbc67961d..24df79eb64 100644 --- a/internal/grpc/services/gateway/ocminvitemanager.go +++ b/internal/grpc/services/gateway/ocminvitemanager.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -43,6 +43,22 @@ func (s *svc) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInv return res, nil } +func (s *svc) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) { + c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint) + if err != nil { + return &invitepb.ListInviteTokensResponse{ + Status: status.NewInternal(ctx, "error getting user invite provider client"), + }, nil + } + + res, err := c.ListInviteTokens(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling ListInviteTokens") + } + + return res, nil +} + func (s *svc) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) { c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint) if err != nil { @@ -106,3 +122,19 @@ func (s *svc) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedU return res, nil } + +func (s *svc) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) { + c, err := pool.GetOCMInviteManagerClient(s.c.OCMInviteManagerEndpoint) + if err != nil { + return &invitepb.DeleteAcceptedUserResponse{ + Status: status.NewInternal(ctx, "error getting user invite provider client"), + }, nil + } + + res, err := c.DeleteAcceptedUser(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling FindAcceptedUsers") + } + + return res, nil +} diff --git a/internal/grpc/services/gateway/ocmproviderauthorizer.go b/internal/grpc/services/gateway/ocmproviderauthorizer.go index 91a3d83dba..23077912d2 100644 --- a/internal/grpc/services/gateway/ocmproviderauthorizer.go +++ b/internal/grpc/services/gateway/ocmproviderauthorizer.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/internal/grpc/services/gateway/ocmshareprovider.go b/internal/grpc/services/gateway/ocmshareprovider.go index cd42d68079..6ee1182c4e 100644 --- a/internal/grpc/services/gateway/ocmshareprovider.go +++ b/internal/grpc/services/gateway/ocmshareprovider.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -25,12 +25,10 @@ import ( "path" "strings" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" 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" datatx "github.com/cs3org/go-cs3apis/cs3/tx/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/appctx" ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/errtypes" @@ -53,16 +51,22 @@ func (s *svc) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest return nil, errors.Wrap(err, "gateway: error calling CreateShare") } - if s.c.CommitShareToStorageGrant { - addGrantStatus, err := s.addGrant(ctx, req.ResourceId, req.Grant.Grantee, req.Grant.Permissions.Permissions, nil, nil) - if err != nil { - return nil, errors.Wrap(err, "gateway: error adding OCM grant to storage") - } - if addGrantStatus.Code != rpc.Code_CODE_OK { - return &ocm.CreateOCMShareResponse{ - Status: addGrantStatus, - }, err - } + status, err := s.addGrant(ctx, req.ResourceId, req.Grantee, req.AccessMethods[0].GetWebdavOptions().Permissions, req.Expiration, nil) + if err != nil { + appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg(err.Error()) + return nil, errors.Wrap(err, "gateway: error adding grant to storage") + } + + switch status.Code { + case rpc.Code_CODE_OK: + s.statCache.RemoveStatContext(ctx, ctxpkg.ContextMustGetUser(ctx).GetId(), req.ResourceId) + case rpc.Code_CODE_UNIMPLEMENTED: + appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants not supported, ignoring") + default: + appctx.GetLogger(ctx).Debug().Interface("status", status).Interface("req", req).Msg("storing grants is not successful") + return &ocm.CreateOCMShareResponse{ + Status: status, + }, err } return res, nil @@ -76,44 +80,11 @@ func (s *svc) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest }, nil } - // if we need to commit the share, we need the resource it points to. - var share *ocm.Share - if s.c.CommitShareToStorageGrant { - getShareReq := &ocm.GetOCMShareRequest{ - Ref: req.Ref, - } - getShareRes, err := c.GetOCMShare(ctx, getShareReq) - if err != nil { - return nil, errors.Wrap(err, "gateway: error calling GetShare") - } - - if getShareRes.Status.Code != rpc.Code_CODE_OK { - res := &ocm.RemoveOCMShareResponse{ - Status: status.NewInternal(ctx, - "error getting share when committing to the storage"), - } - return res, nil - } - share = getShareRes.Share - } - res, err := c.RemoveOCMShare(ctx, req) if err != nil { return nil, errors.Wrap(err, "gateway: error calling RemoveShare") } - if s.c.CommitShareToStorageGrant { - removeGrantStatus, err := s.removeGrant(ctx, share.ResourceId, share.Grantee, share.Permissions.Permissions, nil) - if err != nil { - return nil, errors.Wrap(err, "gateway: error removing OCM grant from storage") - } - if removeGrantStatus.Code != rpc.Code_CODE_OK { - return &ocm.RemoveOCMShareResponse{ - Status: removeGrantStatus, - }, err - } - } - return res, nil } @@ -127,6 +98,7 @@ func (s *svc) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) { c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") return &ocm.GetOCMShareResponse{ Status: status.NewInternal(ctx, "error getting user share provider client"), }, nil @@ -140,10 +112,25 @@ func (s *svc) getOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*oc return res, nil } +func (s *svc) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) { + c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetOCMShareProviderClient") + } + + res, err := c.GetOCMShareByToken(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetOCMShareByToken") + } + + return res, nil +} + // TODO(labkode): read GetShare comment. func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) { c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") return &ocm.ListOCMSharesResponse{ Status: status.NewInternal(ctx, "error getting user share provider client"), }, nil @@ -160,6 +147,7 @@ func (s *svc) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) { c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") return &ocm.UpdateOCMShareResponse{ Status: status.NewInternal(ctx, "error getting share provider client"), }, nil @@ -176,6 +164,7 @@ func (s *svc) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest func (s *svc) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) { c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") return &ocm.ListReceivedOCMSharesResponse{ Status: status.NewInternal(ctx, "error getting share provider client"), }, nil @@ -193,190 +182,70 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive log := appctx.GetLogger(ctx) c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") return &ocm.UpdateReceivedOCMShareResponse{ Status: status.NewInternal(ctx, "error getting share provider client"), }, nil } - res, err := c.UpdateReceivedOCMShare(ctx, req) + // retrieve the current received share + getShareReq := &ocm.GetReceivedOCMShareRequest{ + Ref: &ocm.ShareReference{ + Spec: &ocm.ShareReference_Id{ + Id: req.Share.Id, + }, + }, + } + getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq) if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare") + log.Error().Err(err).Msg("gateway: error calling GetReceivedOCMShare") return &ocm.UpdateReceivedOCMShareResponse{ Status: &rpc.Status{ Code: rpc.Code_CODE_INTERNAL, }, }, nil } + if getShareRes.Status.Code != rpc.Code_CODE_OK { + log.Error().Msg("gateway: error calling GetReceivedOCMShare") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_INTERNAL, + Message: "gateway: error calling GetReceivedOCMShare", + }, + }, nil + } + share := getShareRes.Share + if share == nil { + log.Error().Err(err).Msg("gateway: got a nil share from GetReceivedOCMShare") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_INTERNAL, + Message: "gateway: got a nil share from GetReceivedOCMShare", + }, + }, nil + } - // if we don't need to create/delete references then we return early. - if !s.c.CommitShareToStorageGrant { - return res, nil + res, err := c.UpdateReceivedOCMShare(ctx, req) + if err != nil { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_INTERNAL, + }, + }, nil } - // properties are updated in the order they appear in the field mask - // when an error occurs the request ends and no further fields are updated for i := range req.UpdateMask.Paths { switch req.UpdateMask.Paths[i] { case "state": switch req.GetShare().GetState() { case ocm.ShareState_SHARE_STATE_ACCEPTED: - getShareReq := &ocm.GetReceivedOCMShareRequest{ - Ref: &ocm.ShareReference{ - Spec: &ocm.ShareReference_Id{ - Id: req.Share.Share.Id, - }, - }, - } - getShareRes, err := s.GetReceivedOCMShare(ctx, getShareReq) - if err != nil { - log.Err(err).Msg("gateway: error calling GetReceivedShare") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - }, - }, nil - } - - if getShareRes.Status.Code != rpc.Code_CODE_OK { - log.Error().Msg("gateway: error calling GetReceivedShare") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - }, - }, nil - } - - share := getShareRes.Share - if share == nil { - panic("gateway: error updating a received share: the share is nil") - } - - if share.GetShare().ShareType == ocm.Share_SHARE_TYPE_TRANSFER { - srcIdp := share.GetShare().GetOwner().GetIdp() - meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ - Domain: srcIdp, - }) - if err != nil { - log.Err(err).Msg("gateway: error calling GetInfoByDomain") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, - }, nil - } - var srcEndpoint string - var srcEndpointBaseURI string - // target URI scheme will be the webdav endpoint scheme - var srcEndpointScheme string - for _, s := range meshProvider.ProviderInfo.Services { - if strings.ToLower(s.Endpoint.Type.Name) == "webdav" { - url, err := url.Parse(s.Endpoint.Path) - if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint " + s.Endpoint.Path) - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, - }, nil - } - srcEndpoint = url.Host - srcEndpointBaseURI = url.Path - srcEndpointScheme = url.Scheme - break - } - } - - var srcToken string - srcTokenOpaque, ok := share.GetShare().Grantee.Opaque.Map["token"] - if !ok { - return &ocm.UpdateReceivedOCMShareResponse{ - Status: status.NewNotFound(ctx, "token not found"), - }, nil - } - switch srcTokenOpaque.Decoder { - case "plain": - srcToken = string(srcTokenOpaque.Value) - default: - return &ocm.UpdateReceivedOCMShareResponse{ - Status: status.NewInternal(ctx, "error updating received share"), - }, nil - } - - srcPath := path.Join(srcEndpointBaseURI, share.GetShare().Name) - srcTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", srcEndpointScheme, srcToken, srcEndpoint, srcPath) - - // get the webdav endpoint of the grantee's idp - var granteeIdp string - if share.GetShare().GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER { - granteeIdp = share.GetShare().GetGrantee().GetUserId().Idp - } - if share.GetShare().GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP { - granteeIdp = share.GetShare().GetGrantee().GetGroupId().Idp - } - destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp) - if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, - }, nil - } - url, err := url.Parse(destWebdavEndpoint) - if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint " + destWebdavEndpoint) - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, - }, nil - } - destEndpoint := url.Host - destEndpointBaseURI := url.Path - destEndpointScheme := url.Scheme - destToken := ctxpkg.ContextMustGetToken(ctx) - homeRes, err := s.GetHome(ctx, &provider.GetHomeRequest{}) - if err != nil { - log.Err(err).Msg("gateway: error calling UpdateReceivedShare") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, - }, nil - } - destPath := path.Join(destEndpointBaseURI, homeRes.Path, s.c.DataTransfersFolder, path.Base(share.GetShare().Name)) - destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destEndpoint, destPath) - - opaqueObj := &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "shareId": { - Decoder: "plain", - Value: []byte(share.GetShare().GetId().OpaqueId), - }, - }, - } - req := &datatx.CreateTransferRequest{ - SrcTargetUri: srcTargetURI, - DestTargetUri: destTargetURI, - Opaque: opaqueObj, - } - res, err := s.CreateTransfer(ctx, req) - if err != nil { - log.Err(err).Msg("gateway: error calling PullTransfer") - return &ocm.UpdateReceivedOCMShareResponse{ - Status: &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, - }, - }, err - } - - log.Info().Msgf("gateway: PullTransfer: %v", res.TxInfo) - - // do not create an OCM reference, just return - return &ocm.UpdateReceivedOCMShareResponse{ - Status: status.NewOK(ctx), - }, nil - } - - createRefStatus, err := s.createOCMReference(ctx, share.Share) - return &ocm.UpdateReceivedOCMShareResponse{ - Status: createRefStatus, - }, err + // for a transfer this is handled elsewhere + case ocm.ShareState_SHARE_STATE_PENDING: + // currently no consequences case ocm.ShareState_SHARE_STATE_REJECTED: - return &ocm.UpdateReceivedOCMShareResponse{ - Status: status.NewUnimplemented(ctx, err, "ocm share rejection not supported at the moment"), - }, nil - + // TODO + return res, nil } case "mount_point": // TODO(labkode): implementing updating mount point @@ -388,115 +257,157 @@ func (s *svc) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceive return nil, errtypes.NotSupported("updating " + req.UpdateMask.Paths[i] + " is not supported") } } - return res, nil -} - -func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) { - c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) - if err != nil { - return &ocm.GetReceivedOCMShareResponse{ - Status: status.NewInternal(ctx, "error getting share provider client"), - }, nil - } + // handle transfer in case it has not already been accepted + if s.isTransferShare(share) && req.GetShare().State == ocm.ShareState_SHARE_STATE_ACCEPTED { + if share.State == ocm.ShareState_SHARE_STATE_ACCEPTED { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare, share already accepted.") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_FAILED_PRECONDITION, + Message: "Share already accepted.", + }, + }, err + } + // get provided destination path + transferDestinationPath, err := s.getTransferDestinationPath(ctx, req) + if err != nil { + if err != nil { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_INTERNAL, + }, + }, err + } + } - res, err := c.GetReceivedOCMShare(ctx, req) - if err != nil { - return nil, errors.Wrap(err, "gateway: error calling GetReceivedShare") + error := s.handleTransfer(ctx, share, transferDestinationPath) + if error != nil { + log.Err(error).Msg("gateway: error handling transfer in UpdateReceivedShare") + return &ocm.UpdateReceivedOCMShareResponse{ + Status: &rpc.Status{ + Code: rpc.Code_CODE_INTERNAL, + }, + }, error + } } - return res, nil } -func (s *svc) createOCMReference(ctx context.Context, share *ocm.Share) (*rpc.Status, error) { - +func (s *svc) handleTransfer(ctx context.Context, share *ocm.ReceivedShare, transferDestinationPath string) error { log := appctx.GetLogger(ctx) - var token string - tokenOpaque, ok := share.Grantee.Opaque.Map["token"] + protocol, ok := s.getTransferProtocol(share) if !ok { - log.Debug().Msg("createOCMReference: token not found in opaque map") - return status.NewNotFound(ctx, "token not found"), nil - } - switch tokenOpaque.Decoder { - case "plain": - token = string(tokenOpaque.Value) - default: - log.Error().Str("decoder", tokenOpaque.Decoder).Msg("createOCMReference: opaque entry decoder not recognized") - return status.NewInternal(ctx, "invalid opaque entry decoder"), nil + return errors.New("gateway: unable to retrieve transfer protocol") } + sourceURI := protocol.SourceUri - homeRes, err := s.GetHome(ctx, &provider.GetHomeRequest{}) + // get the webdav endpoint of the grantee's idp + var granteeIdp string + if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_USER { + granteeIdp = share.GetGrantee().GetUserId().Idp + } + if share.GetGrantee().Type == provider.GranteeType_GRANTEE_TYPE_GROUP { + granteeIdp = share.GetGrantee().GetGroupId().Idp + } + destWebdavEndpoint, err := s.getWebdavEndpoint(ctx, granteeIdp) if err != nil { - log.Error().Err(err).Msg("createOCMReference: error calling GetHome") - return status.NewInternal(ctx, "error updating received share"), nil + log.Err(err).Msg("gateway: error calling UpdateReceivedShare") + return err } - - var refPath, targetURI string - if share.ShareType == ocm.Share_SHARE_TYPE_TRANSFER { - createTransferDir, err := s.CreateContainer(ctx, &provider.CreateContainerRequest{ - Ref: &provider.Reference{ - Path: path.Join(homeRes.Path, s.c.DataTransfersFolder), - }, - }) - if err != nil { - log.Error().Err(err).Msg("createOCMReference: error creating transfers directory") - return status.NewInternal(ctx, "error creating transfers directory"), nil - } - if createTransferDir.Status.Code != rpc.Code_CODE_OK && createTransferDir.Status.Code != rpc.Code_CODE_ALREADY_EXISTS { - log.Error().Interface("status", createTransferDir.Status).Msg("createOCMReference: error creating transfers directory") - return status.NewInternal(ctx, "error creating transfers directory"), nil - } - - refPath = path.Join(homeRes.Path, s.c.DataTransfersFolder, path.Base(share.Name)) - targetURI = fmt.Sprintf("datatx://%s@%s?name=%s", token, share.Creator.Idp, share.Name) + destWebdavEndpointURL, err := url.Parse(destWebdavEndpoint) + if err != nil { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav endpoint \"" + destWebdavEndpoint + "\" into URL structure") + return err + } + destWebdavHost, err := s.getWebdavHost(ctx, granteeIdp) + if err != nil { + log.Err(err).Msg("gateway: error calling UpdateReceivedShare") + return err + } + var dstWebdavURLString string + if strings.Contains(destWebdavHost, "://") { + dstWebdavURLString = destWebdavHost } else { - // reference path is the home path + some name on the corresponding - // mesh provider (/home/MyShares/x) - // It is the responsibility of the gateway to resolve these references and merge the response back - // from the main request. - refPath = path.Join(homeRes.Path, s.c.ShareFolder, path.Base(share.Name)) - // webdav is the scheme, token@host the opaque part and the share name the query of the URL. - targetURI = fmt.Sprintf("webdav://%s@%s?name=%s", token, share.Creator.Idp, share.Name) + dstWebdavURLString = "http://" + destWebdavHost } - - log.Info().Msg("mount path will be:" + refPath) - - c, p, err := s.findByPath(ctx, refPath) + dstWebdavHostURL, err := url.Parse(dstWebdavURLString) if err != nil { - log.Error().Err(err).Msg("createOCMReference: could not find storage provider") - if _, ok := err.(errtypes.IsNotFound); ok { - return status.NewNotFound(ctx, "storage provider not found"), nil - } - return status.NewInternal(ctx, "error finding storage provider"), nil + log.Err(err).Msg("gateway: error calling UpdateReceivedShare: unable to parse webdav service host \"" + dstWebdavURLString + "\" into URL structure") + return err + } + destServiceHost := dstWebdavHostURL.Host + dstWebdavHostURL.Path + // optional prefix must only appear in target url path: + // http://...token...@reva.eu/prefix/?name=remote.php/webdav/home/... + destEndpointPath := strings.TrimPrefix(destWebdavEndpointURL.Path, dstWebdavHostURL.Path) + destEndpointScheme := destWebdavEndpointURL.Scheme + destToken := ctxpkg.ContextMustGetToken(ctx) + destPath := path.Join(destEndpointPath, transferDestinationPath, path.Base(share.Name)) + destTargetURI := fmt.Sprintf("%s://%s@%s?name=%s", destEndpointScheme, destToken, destServiceHost, destPath) + // var destUri string + req := &datatx.CreateTransferRequest{ + SrcTargetUri: sourceURI, + DestTargetUri: destTargetURI, + ShareId: share.Id, } - var ( - root *provider.ResourceId - mountPath string - ) - for _, space := range decodeSpaces(p) { - mountPath = decodePath(space) - root = space.Root - break // TODO can there be more than one space for a path? + res, err := s.CreateTransfer(ctx, req) + if err != nil { + return err } + log.Info().Msgf("gateway: CreateTransfer: %v", res.TxInfo) + return nil +} + +func (s *svc) isTransferShare(share *ocm.ReceivedShare) bool { + _, ok := s.getTransferProtocol(share) + return ok +} - pRef := unwrap(&provider.Reference{Path: refPath}, mountPath, root) - createRefReq := &provider.CreateReferenceRequest{ - Ref: pRef, - TargetUri: targetURI, +func (s *svc) getTransferDestinationPath(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (string, error) { + log := appctx.GetLogger(ctx) + // the destination path is not part of any protocol, but an opaque field + destPathOpaque, ok := req.GetOpaque().GetMap()["transfer_destination_path"] + if ok { + switch destPathOpaque.Decoder { + case "plain": + if string(destPathOpaque.Value) != "" { + return string(destPathOpaque.Value), nil + } + default: + return "", errtypes.NotSupported("decoder of opaque entry 'transfer_destination_path' not recognized: " + destPathOpaque.Decoder) + } + } + log.Info().Msg("destination path not provided, trying default transfer destination folder") + if s.c.DataTransfersFolder == "" { + return "", errtypes.NotSupported("no destination path provided and default transfer destination folder is not set") } - createRefRes, err := c.CreateReference(ctx, createRefReq) + return s.c.DataTransfersFolder, nil +} + +func (s *svc) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) { + c, err := pool.GetOCMShareProviderClient(s.c.OCMShareProviderEndpoint) if err != nil { - log.Err(err).Msg("createOCMReference: error calling CreateReference") - return &rpc.Status{ - Code: rpc.Code_CODE_INTERNAL, + appctx.GetLogger(ctx).Error().Err(err).Msg("error calling GetOCMShareProviderClient") + return &ocm.GetReceivedOCMShareResponse{ + Status: status.NewInternal(ctx, "error getting share provider client"), }, nil } - if createRefRes.Status.Code != rpc.Code_CODE_OK { - log.Error().Interface("status", createRefRes.Status).Msg("createOCMReference: error calling CreateReference") - return status.NewInternal(ctx, "error updating received share"), nil + res, err := c.GetReceivedOCMShare(ctx, req) + if err != nil { + return nil, errors.Wrap(err, "gateway: error calling GetReceivedShare") } - return status.NewOK(ctx), nil + return res, nil +} + +func (s *svc) getTransferProtocol(share *ocm.ReceivedShare) (*ocm.TransferProtocol, bool) { + for _, p := range share.Protocols { + if d, ok := p.Term.(*ocm.Protocol_TransferOptions); ok { + return d.TransferOptions, true + } + } + return nil, false } diff --git a/internal/grpc/services/gateway/storageprovider.go b/internal/grpc/services/gateway/storageprovider.go index 0c8d0425cc..a4ad2af393 100644 --- a/internal/grpc/services/gateway/storageprovider.go +++ b/internal/grpc/services/gateway/storageprovider.go @@ -1030,11 +1030,6 @@ func (s *svc) GetQuota(ctx context.Context, req *gateway.GetQuotaRequest) (*prov return res, nil } -func (s *svc) findByPath(ctx context.Context, path string) (provider.ProviderAPIClient, *registry.ProviderInfo, error) { - ref := &provider.Reference{Path: path} - return s.find(ctx, ref) -} - // find looks up the provider that is responsible for the given request // It will return a client that the caller can use to make the call, as well as the ProviderInfo. It: // - contains the provider path, which is the mount point of the provider diff --git a/internal/grpc/services/gateway/webdavstorageprovider.go b/internal/grpc/services/gateway/webdavstorageprovider.go index 0bc75685ec..e0835f8acc 100644 --- a/internal/grpc/services/gateway/webdavstorageprovider.go +++ b/internal/grpc/services/gateway/webdavstorageprovider.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -60,22 +60,22 @@ func (s *svc) extractEndpointInfo(ctx context.Context, targetURL string) (*webda }, nil } -func appendNameQuery(targetURL string, nameQueries ...string) (string, error) { - uri, err := url.Parse(targetURL) +func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, error) { + meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + Domain: domain, + }) if err != nil { - return "", err + return "", errors.Wrap(err, "gateway: error calling GetInfoByDomain") } - q, err := url.ParseQuery(uri.RawQuery) - if err != nil { - return "", err + for _, s := range meshProvider.ProviderInfo.Services { + if strings.ToLower(s.Endpoint.Type.Name) == "webdav" { + return s.Endpoint.Path, nil + } } - name := append([]string{q["name"][0]}, nameQueries...) - q.Set("name", path.Join(name...)) - uri.RawQuery = q.Encode() - return uri.String(), nil + return "", errtypes.NotFound(domain) } -func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, error) { +func (s *svc) getWebdavHost(ctx context.Context, domain string) (string, error) { meshProvider, err := s.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ Domain: domain, }) @@ -84,8 +84,23 @@ func (s *svc) getWebdavEndpoint(ctx context.Context, domain string) (string, err } for _, s := range meshProvider.ProviderInfo.Services { if strings.ToLower(s.Endpoint.Type.Name) == "webdav" { - return s.Endpoint.Path, nil + return s.Host, nil } } return "", errtypes.NotFound(domain) } + +func appendNameQuery(targetURL string, nameQueries ...string) (string, error) { + uri, err := url.Parse(targetURL) + if err != nil { + return "", err + } + q, err := url.ParseQuery(uri.RawQuery) + if err != nil { + return "", err + } + name := append([]string{q["name"][0]}, nameQueries...) + q.Set("name", path.Join(name...)) + uri.RawQuery = q.Encode() + return uri.String(), nil +} diff --git a/internal/grpc/services/ocmcore/ocmcore.go b/internal/grpc/services/ocmcore/ocmcore.go index 9782688f3d..f3d7e7813b 100644 --- a/internal/grpc/services/ocmcore/ocmcore.go +++ b/internal/grpc/services/ocmcore/ocmcore.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,20 +20,19 @@ package ocmcore import ( "context" - "encoding/json" "fmt" + "time" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" - provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" - typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/ocm/share" - "github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry" + "github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc/status" - "github.com/mitchellh/mapstructure" - "github.com/pkg/errors" + "github.com/cs3org/reva/v2/pkg/utils/cfg" "google.golang.org/grpc" ) @@ -48,10 +47,10 @@ type config struct { type service struct { conf *config - sm share.Manager + repo share.Repository } -func (c *config) init() { +func (c *config) ApplyDefaults() { if c.Driver == "" { c.Driver = "json" } @@ -61,39 +60,28 @@ func (s *service) Register(ss *grpc.Server) { ocmcore.RegisterOcmCoreAPIServer(ss, s) } -func getShareManager(c *config) (share.Manager, error) { +func getShareRepository(c *config) (share.Repository, error) { if f, ok := registry.NewFuncs[c.Driver]; ok { return f(c.Drivers[c.Driver]) } return nil, errtypes.NotFound(fmt.Sprintf("driver not found: %s", c.Driver)) } -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 creates a new ocm core svc +// New creates a new ocm core svc. func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { - - c, err := parseConfig(m) - if err != nil { + var c config + if err := cfg.Decode(m, &c); err != nil { return nil, err } - c.init() - sm, err := getShareManager(c) + repo, err := getShareRepository(&c) if err != nil { return nil, err } service := &service{ - conf: c, - sm: sm, + conf: &c, + repo: repo, } return service, nil @@ -107,91 +95,53 @@ func (s *service) UnprotectedEndpoints() []string { return []string{"/cs3.ocm.core.v1beta1.OcmCoreAPI/CreateOCMCoreShare"} } -// CreateOCMCoreShare is called when an OCM request comes into this reva instance from +// CreateOCMCoreShare is called when an OCM request comes into this reva instance from. func (s *service) CreateOCMCoreShare(ctx context.Context, req *ocmcore.CreateOCMCoreShareRequest) (*ocmcore.CreateOCMCoreShareResponse, error) { - resource := &provider.ResourceId{ - StorageId: "remote", - OpaqueId: req.Name, + if req.ShareType != ocm.ShareType_SHARE_TYPE_USER { + return nil, errtypes.NotSupported("share type not supported") } - var resourcePermissions *provider.ResourcePermissions - permOpaque, ok := req.Protocol.Opaque.Map["permissions"] - if !ok { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "resource permissions not set"), - }, nil - } - switch permOpaque.Decoder { - case "json": - err := json.Unmarshal(permOpaque.Value, &resourcePermissions) - if err != nil { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "error decoding resource permissions"), - }, nil - } - default: - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "invalid opaque entry decoder"), - }, nil + now := &typesv1beta1.Timestamp{ + Seconds: uint64(time.Now().Unix()), } - var token string - tokenOpaque, ok := req.Protocol.Opaque.Map["token"] - if !ok { - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "token not set"), - }, nil - } - switch tokenOpaque.Decoder { - case "plain": - token = string(tokenOpaque.Value) - default: - return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "invalid opaque entry decoder"), - }, nil - } - - grant := &ocm.ShareGrant{ - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - // For now, we only support user shares. - // TODO (ishank011): To be updated once this is decided. - Id: &provider.Grantee_UserId{UserId: req.ShareWith}, - // passing this in grant.Grantee.Opaque because ShareGrant itself doesn't have a root opaque. - Opaque: &typespb.Opaque{ - Map: map[string]*typespb.OpaqueEntry{ - "remoteShareId": { - Decoder: "plain", - Value: []byte(req.ProviderId), - }, - }, + share, err := s.repo.StoreReceivedShare(ctx, &ocm.ReceivedShare{ + RemoteShareId: req.ResourceId, + Name: req.Name, + Grantee: &providerpb.Grantee{ + Type: providerpb.GranteeType_GRANTEE_TYPE_USER, + Id: &providerpb.Grantee_UserId{ + UserId: req.ShareWith, }, }, - Permissions: &ocm.SharePermissions{ - Permissions: resourcePermissions, - }, - } - - var shareType ocm.Share_ShareType - switch req.Protocol.Name { - case "datatx": - shareType = ocm.Share_SHARE_TYPE_TRANSFER - default: - shareType = ocm.Share_SHARE_TYPE_REGULAR - } - - share, err := s.sm.Share(ctx, resource, grant, req.Name, nil, "", req.Owner, token, shareType) - + ResourceType: req.ResourceType, + ShareType: req.ShareType, + Owner: req.Owner, + Creator: req.Sender, + Protocols: req.Protocols, + Ctime: now, + Mtime: now, + Expiration: req.Expiration, + State: ocm.ShareState_SHARE_STATE_PENDING, + }) if err != nil { + // TODO: identify errors return &ocmcore.CreateOCMCoreShareResponse{ - Status: status.NewInternal(ctx, "error creating ocm core share"), + Status: status.NewInternal(ctx, err.Error()), }, nil } - res := &ocmcore.CreateOCMCoreShareResponse{ + return &ocmcore.CreateOCMCoreShareResponse{ Status: status.NewOK(ctx), Id: share.Id.OpaqueId, Created: share.Ctime, - } - return res, nil + }, nil +} + +func (s *service) UpdateOCMCoreShare(ctx context.Context, req *ocmcore.UpdateOCMCoreShareRequest) (*ocmcore.UpdateOCMCoreShareResponse, error) { + return nil, errtypes.NotSupported("not implemented") +} + +func (s *service) DeleteOCMCoreShare(ctx context.Context, req *ocmcore.DeleteOCMCoreShareRequest) (*ocmcore.DeleteOCMCoreShareResponse, error) { + return nil, errtypes.NotSupported("not implemented") } diff --git a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go index 7af7125c28..671f864098 100644 --- a/internal/grpc/services/ocminvitemanager/ocminvitemanager.go +++ b/internal/grpc/services/ocminvitemanager/ocminvitemanager.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,14 +20,24 @@ package ocminvitemanager import ( "context" + "time" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/ocm/client" "github.com/cs3org/reva/v2/pkg/ocm/invite" - "github.com/cs3org/reva/v2/pkg/ocm/invite/manager/registry" + "github.com/cs3org/reva/v2/pkg/ocm/invite/repository/registry" "github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc/status" - "github.com/mitchellh/mapstructure" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/cs3org/reva/v2/pkg/utils/cfg" "github.com/pkg/errors" "google.golang.org/grpc" ) @@ -37,58 +47,77 @@ func init() { } type config struct { - Driver string `mapstructure:"driver"` - Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + TokenExpiration string `mapstructure:"token_expiration"` + OCMClientTimeout int `mapstructure:"ocm_timeout"` + OCMClientInsecure bool `mapstructure:"ocm_insecure"` + GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"` + ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"` + + tokenExpiration time.Duration } type service struct { - conf *config - im invite.Manager + conf *config + repo invite.Repository + ocmClient *client.OCMClient + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] } -func (c *config) init() { +func (c *config) ApplyDefaults() { if c.Driver == "" { c.Driver = "json" } + if c.TokenExpiration == "" { + c.TokenExpiration = "24h" + } + + c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) } func (s *service) Register(ss *grpc.Server) { invitepb.RegisterInviteAPIServer(ss, s) } -func getInviteManager(c *config) (invite.Manager, error) { +func getInviteRepository(c *config) (invite.Repository, error) { if f, ok := registry.NewFuncs[c.Driver]; ok { return f(c.Drivers[c.Driver]) } return nil, errtypes.NotFound("driver not found: " + c.Driver) } -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") +// New creates a new OCM invite manager svc. +func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + var c config + if err := cfg.Decode(m, &c); err != nil { return nil, err } - return c, nil -} -// New creates a new OCM invite manager svc -func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + p, err := time.ParseDuration(c.TokenExpiration) + if err != nil { + return nil, err + } + c.tokenExpiration = p - c, err := parseConfig(m) + repo, err := getInviteRepository(&c) if err != nil { return nil, err } - c.init() - im, err := getInviteManager(c) + gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC) if err != nil { return nil, err } service := &service{ - conf: c, - im: im, + conf: &c, + repo: repo, + ocmClient: client.New(&client.Config{ + Timeout: time.Duration(c.OCMClientTimeout) * time.Second, + Insecure: c.OCMClientInsecure, + }), + gatewaySelector: gatewaySelector, } return service, nil } @@ -98,12 +127,14 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite"} + return []string{"/cs3.ocm.invite.v1beta1.InviteAPI/AcceptInvite", "/cs3.ocm.invite.v1beta1.InviteAPI/GetAcceptedUser"} } func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.GenerateInviteTokenRequest) (*invitepb.GenerateInviteTokenResponse, error) { - token, err := s.im.GenerateToken(ctx) - if err != nil { + user := ctxpkg.ContextMustGetUser(ctx) + token := CreateToken(s.conf.tokenExpiration, user.GetId(), req.Description) + + if err := s.repo.AddToken(ctx, token); err != nil { return &invitepb.GenerateInviteTokenResponse{ Status: status.NewInternal(ctx, "error generating invite token"), }, nil @@ -115,34 +146,175 @@ func (s *service) GenerateInviteToken(ctx context.Context, req *invitepb.Generat }, nil } -func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) { - err := s.im.ForwardInvite(ctx, req.InviteToken, req.OriginSystemProvider) +func (s *service) ListInviteTokens(ctx context.Context, req *invitepb.ListInviteTokensRequest) (*invitepb.ListInviteTokensResponse, error) { + user := ctxpkg.ContextMustGetUser(ctx) + tokens, err := s.repo.ListTokens(ctx, user.Id) if err != nil { - return &invitepb.ForwardInviteResponse{ - Status: status.NewInternal(ctx, "error forwarding invite"), + return &invitepb.ListInviteTokensResponse{ + Status: status.NewInternal(ctx, "error listing tokens"), }, nil } + return &invitepb.ListInviteTokensResponse{ + Status: status.NewOK(ctx), + InviteTokens: tokens, + }, nil +} + +func (s *service) ForwardInvite(ctx context.Context, req *invitepb.ForwardInviteRequest) (*invitepb.ForwardInviteResponse, error) { + user := ctxpkg.ContextMustGetUser(ctx) + + ocmEndpoint, err := getOCMEndpoint(req.GetOriginSystemProvider()) + if err != nil { + return nil, err + } + + remoteUser, err := s.ocmClient.InviteAccepted(ctx, ocmEndpoint, &client.InviteAcceptedRequest{ + Token: req.InviteToken.GetToken(), + RecipientProvider: s.conf.ProviderDomain, + UserID: user.GetId().GetOpaqueId(), + Email: user.GetMail(), + Name: user.GetDisplayName(), + }) + if err != nil { + switch { + case errors.Is(err, client.ErrTokenInvalid): + return &invitepb.ForwardInviteResponse{ + Status: status.NewInvalid(ctx, "token not valid"), + }, nil + case errors.Is(err, client.ErrTokenNotFound): + return &invitepb.ForwardInviteResponse{ + Status: status.NewNotFound(ctx, "token not found"), + }, nil + case errors.Is(err, client.ErrUserAlreadyAccepted): + return &invitepb.ForwardInviteResponse{ + Status: status.NewAlreadyExists(ctx, err, err.Error()), + }, nil + case errors.Is(err, client.ErrServiceNotTrusted): + return &invitepb.ForwardInviteResponse{ + Status: status.NewPermissionDenied(ctx, err, err.Error()), + }, nil + default: + return &invitepb.ForwardInviteResponse{ + Status: status.NewInternal(ctx, err.Error()), + }, nil + } + } + + // create a link between the user that accepted the share (in ctx) + // and the remote one (the initiator), so at the end of the invitation workflow they + // know each other + + remoteUserID := &userpb.UserId{ + Type: userpb.UserType_USER_TYPE_FEDERATED, + Idp: req.GetOriginSystemProvider().Domain, + OpaqueId: remoteUser.UserID, + } + + if err := s.repo.AddRemoteUser(ctx, user.Id, &userpb.User{ + Id: remoteUserID, + Mail: remoteUser.Email, + DisplayName: remoteUser.Name, + }); err != nil { + if !errors.Is(err, invite.ErrUserAlreadyAccepted) { + // skip error if user was already accepted + return &invitepb.ForwardInviteResponse{ + Status: status.NewInternal(ctx, err.Error()), + }, nil + } + } return &invitepb.ForwardInviteResponse{ - Status: status.NewOK(ctx), + Status: status.NewOK(ctx), + UserId: remoteUserID, + Email: remoteUser.Email, + DisplayName: remoteUser.Name, }, nil } +func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) { + for _, s := range originProvider.Services { + if s.Endpoint.Type.Name == "OCM" { + return s.Endpoint.Path, nil + } + } + return "", errors.New("ocm endpoint not specified for mesh provider") +} + func (s *service) AcceptInvite(ctx context.Context, req *invitepb.AcceptInviteRequest) (*invitepb.AcceptInviteResponse, error) { - err := s.im.AcceptInvite(ctx, req.InviteToken, req.RemoteUser) + token, err := s.repo.GetToken(ctx, req.InviteToken.Token) + if err != nil { + if errors.Is(err, invite.ErrTokenNotFound) { + return &invitepb.AcceptInviteResponse{ + Status: status.NewNotFound(ctx, "token not found"), + }, nil + } + return &invitepb.AcceptInviteResponse{ + Status: status.NewInternal(ctx, err.Error()), + }, nil + } + + if !isTokenValid(token) { + return &invitepb.AcceptInviteResponse{ + Status: status.NewInvalid(ctx, "token is not valid"), + }, nil + } + + initiator, err := s.getUserInfo(ctx, token.UserId) if err != nil { return &invitepb.AcceptInviteResponse{ - Status: status.NewInternal(ctx, "error accepting invite"), + Status: status.NewInternal(ctx, err.Error()), + }, nil + } + + if err := s.repo.AddRemoteUser(ctx, token.GetUserId(), req.GetRemoteUser()); err != nil { + if errors.Is(err, invite.ErrUserAlreadyAccepted) { + return &invitepb.AcceptInviteResponse{ + Status: status.NewAlreadyExists(ctx, err, err.Error()), + }, nil + } + return &invitepb.AcceptInviteResponse{ + Status: status.NewInternal(ctx, err.Error()), }, nil } return &invitepb.AcceptInviteResponse{ - Status: status.NewOK(ctx), + Status: status.NewOK(ctx), + UserId: initiator.GetId(), + Email: initiator.Mail, + DisplayName: initiator.DisplayName, }, nil } +func (s *service) getUserInfo(ctx context.Context, id *userpb.UserId) (*userpb.User, error) { + gw, err := s.gatewaySelector.Next() + if err != nil { + return nil, nil + } + res, err := gw.GetUser(ctx, &userpb.GetUserRequest{ + UserId: id, + }) + if err != nil { + return nil, err + } + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + return nil, errors.New(res.Status.Message) + } + + return res.User, nil +} + +func isTokenValid(token *invitepb.InviteToken) bool { + return time.Now().Unix() < int64(token.Expiration.Seconds) +} + func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*invitepb.GetAcceptedUserResponse, error) { - remoteUser, err := s.im.GetAcceptedUser(ctx, req.RemoteUserId) + user, ok := getUserFilter(ctx, req) + if !ok { + return &invitepb.GetAcceptedUserResponse{ + Status: status.NewInvalidArg(ctx, "user not found"), + }, nil + } + remoteUser, err := s.repo.GetRemoteUser(ctx, user.GetId(), req.GetRemoteUserId()) if err != nil { return &invitepb.GetAcceptedUserResponse{ Status: status.NewInternal(ctx, "error fetching remote user details"), @@ -155,11 +327,34 @@ func (s *service) GetAcceptedUser(ctx context.Context, req *invitepb.GetAccepted }, nil } +func getUserFilter(ctx context.Context, req *invitepb.GetAcceptedUserRequest) (*userpb.User, bool) { + user, ok := ctxpkg.ContextGetUser(ctx) + if ok { + return user, true + } + + if req.Opaque == nil || req.Opaque.Map == nil { + return nil, false + } + + v, ok := req.Opaque.Map["user-filter"] + if !ok { + return nil, false + } + + var u userpb.UserId + if err := utils.UnmarshalJSONToProtoV1(v.Value, &u); err != nil { + return nil, false + } + return &userpb.User{Id: &u}, true +} + func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAcceptedUsersRequest) (*invitepb.FindAcceptedUsersResponse, error) { - acceptedUsers, err := s.im.FindAcceptedUsers(ctx, req.Filter) + user := ctxpkg.ContextMustGetUser(ctx) + acceptedUsers, err := s.repo.FindRemoteUsers(ctx, user.GetId(), req.GetFilter()) if err != nil { return &invitepb.FindAcceptedUsersResponse{ - Status: status.NewInternal(ctx, "error finding remote users"), + Status: status.NewInternal(ctx, "error finding remote users: "+err.Error()), }, nil } @@ -168,3 +363,16 @@ func (s *service) FindAcceptedUsers(ctx context.Context, req *invitepb.FindAccep AcceptedUsers: acceptedUsers, }, nil } + +func (s *service) DeleteAcceptedUser(ctx context.Context, req *invitepb.DeleteAcceptedUserRequest) (*invitepb.DeleteAcceptedUserResponse, error) { + user := ctxpkg.ContextMustGetUser(ctx) + if err := s.repo.DeleteRemoteUser(ctx, user.Id, req.RemoteUserId); err != nil { + return &invitepb.DeleteAcceptedUserResponse{ + Status: status.NewInternal(ctx, "error deleting remote users: "+err.Error()), + }, nil + } + + return &invitepb.DeleteAcceptedUserResponse{ + Status: status.NewOK(ctx), + }, nil +} diff --git a/internal/grpc/services/ocminvitemanager/token.go b/internal/grpc/services/ocminvitemanager/token.go new file mode 100644 index 0000000000..d91243c1d5 --- /dev/null +++ b/internal/grpc/services/ocminvitemanager/token.go @@ -0,0 +1,45 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocminvitemanager + +import ( + "time" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/google/uuid" +) + +// CreateToken creates a InviteToken object for the userID indicated by userID. +func CreateToken(expiration time.Duration, userID *userpb.UserId, description string) *invitepb.InviteToken { + tokenID := uuid.New().String() + now := time.Now() + expirationTime := now.Add(expiration) + + return &invitepb.InviteToken{ + Token: tokenID, + UserId: userID, + Expiration: &typesv1beta1.Timestamp{ + Seconds: uint64(expirationTime.Unix()), + Nanos: 0, + }, + Description: description, + } +} diff --git a/internal/grpc/services/ocminvitemanager/token_test.go b/internal/grpc/services/ocminvitemanager/token_test.go new file mode 100644 index 0000000000..cbe57e9e67 --- /dev/null +++ b/internal/grpc/services/ocminvitemanager/token_test.go @@ -0,0 +1,87 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocminvitemanager + +import ( + "sync" + "testing" + "time" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" +) + +func TestCreateToken(t *testing.T) { + user := userpb.User{ + Id: &userpb.UserId{ + Idp: "http://localhost:20080", + OpaqueId: "4c510ada-c86b-4815-8820-42cdf82c3d51", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + } + + token := CreateToken(24*time.Hour, user.GetId(), "") + if token == nil { + t.Errorf("CreateToken() got = %v", token) + } + if token.GetToken() == "" { + t.Errorf("CreateToken() got = %v", token) + } + if token.GetUserId() != user.GetId() { + t.Errorf("CreateToken() got = %v", token) + } +} + +func TestCreateTokenCollision(t *testing.T) { + tokens := sync.Map{} + + user := userpb.User{ + Id: &userpb.UserId{ + Idp: "http://localhost:20080", + OpaqueId: "4c510ada-c86b-4815-8820-42cdf82c3d51", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + } + + for i := 0; i < 1000000; i++ { + token := CreateToken(24*time.Hour, user.GetId(), "") + + if token == nil { + t.Errorf("CreateToken() token = %v", token) + } + + _, ok := tokens.Load(token.GetToken()) + if ok == true { + t.Errorf("CreateToken() there are ID collision = %v", token) + } + + tokens.Store(token.GetToken(), token) + } +} diff --git a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go index 09d9c70510..88d775eafe 100644 --- a/internal/grpc/services/ocmshareprovider/ocmshareprovider.go +++ b/internal/grpc/services/ocmshareprovider/ocmshareprovider.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -20,14 +20,33 @@ package ocmshareprovider import ( "context" - + "fmt" + "net/url" + "path/filepath" + "strings" + "text/template" + "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typespb "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/ocmd" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/ocm/client" "github.com/cs3org/reva/v2/pkg/ocm/share" - "github.com/cs3org/reva/v2/pkg/ocm/share/manager/registry" + "github.com/cs3org/reva/v2/pkg/ocm/share/repository/registry" "github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc/status" - "github.com/mitchellh/mapstructure" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/storage/utils/walker" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/cs3org/reva/v2/pkg/utils/cfg" "github.com/pkg/errors" "google.golang.org/grpc" ) @@ -37,58 +56,85 @@ func init() { } type config struct { - Driver string `mapstructure:"driver"` - Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + Driver string `mapstructure:"driver"` + Drivers map[string]map[string]interface{} `mapstructure:"drivers"` + ClientTimeout int `mapstructure:"client_timeout"` + ClientInsecure bool `mapstructure:"client_insecure"` + GatewaySVC string `mapstructure:"gatewaysvc" validate:"required"` + ProviderDomain string `mapstructure:"provider_domain" validate:"required" docs:"The same domain registered in the provider authorizer"` + WebDAVEndpoint string `mapstructure:"webdav_endpoint" validate:"required"` + WebappTemplate string `mapstructure:"webapp_template"` } type service struct { - conf *config - sm share.Manager + conf *config + repo share.Repository + client *client.OCMClient + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] + webappTmpl *template.Template + walker walker.Walker } -func (c *config) init() { +func (c *config) ApplyDefaults() { if c.Driver == "" { c.Driver = "json" } + if c.ClientTimeout == 0 { + c.ClientTimeout = 10 + } + if c.WebappTemplate == "" { + c.WebappTemplate = "https://cernbox.cern.ch/external/sciencemesh/{{.Token}}{relative-path-to-shared-resource}" + } + + c.GatewaySVC = sharedconf.GetGatewaySVC(c.GatewaySVC) } func (s *service) Register(ss *grpc.Server) { ocm.RegisterOcmAPIServer(ss, s) } -func getShareManager(c *config) (share.Manager, error) { +func getShareRepository(c *config) (share.Repository, error) { if f, ok := registry.NewFuncs[c.Driver]; ok { return f(c.Drivers[c.Driver]) } return nil, errtypes.NotFound("driver not found: " + c.Driver) } -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") +// New creates a new ocm share provider svc. +func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + var c config + if err := cfg.Decode(m, &c); err != nil { return nil, err } - return c, nil -} -// New creates a new ocm share provider svc -func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { + repo, err := getShareRepository(&c) + if err != nil { + return nil, err + } + + client := client.New(&client.Config{ + Timeout: time.Duration(c.ClientTimeout) * time.Second, + Insecure: c.ClientInsecure, + }) - c, err := parseConfig(m) + gatewaySelector, err := pool.GatewaySelector(c.GatewaySVC) if err != nil { return nil, err } - c.init() - sm, err := getShareManager(c) + tpl, err := template.New("webapp_template").Parse(c.WebappTemplate) if err != nil { return nil, err } + walker := walker.NewWalker(gatewaySelector) service := &service{ - conf: c, - sm: sm, + conf: &c, + repo: repo, + client: client, + gatewaySelector: gatewaySelector, + webappTmpl: tpl, + walker: walker, } return service, nil @@ -99,98 +145,240 @@ func (s *service) Close() error { } func (s *service) UnprotectedEndpoints() []string { - return []string{} + return []string{"/cs3.sharing.ocm.v1beta1.OcmAPI/GetOCMShareByToken"} +} + +func getOCMEndpoint(originProvider *ocmprovider.ProviderInfo) (string, error) { + for _, s := range originProvider.Services { + if s.Endpoint.Type.Name == "OCM" { + return s.Endpoint.Path, nil + } + } + return "", errors.New("ocm endpoint not specified for mesh provider") +} + +func formatOCMUser(u *userpb.UserId) string { + return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp) +} + +func getResourceType(info *providerpb.ResourceInfo) string { + switch info.Type { + case providerpb.ResourceType_RESOURCE_TYPE_FILE: + return "file" + case providerpb.ResourceType_RESOURCE_TYPE_CONTAINER: + return "folder" + } + return "unknown" +} + +func (s *service) webdavURL(ctx context.Context, share *ocm.Share) string { + // the url is in the form of https://cernbox.cern.ch/remote.php/dav/ocm/token + p, _ := url.JoinPath(s.conf.WebDAVEndpoint, "/dav/ocm", share.Token) + return p +} + +func (s *service) getWebdavProtocol(ctx context.Context, share *ocm.Share, m *ocm.AccessMethod_WebdavOptions) *ocmd.WebDAV { + var perms []string + if m.WebdavOptions.Permissions.InitiateFileDownload { + perms = append(perms, "read") + } + if m.WebdavOptions.Permissions.InitiateFileUpload { + perms = append(perms, "write") + } + + return &ocmd.WebDAV{ + Permissions: perms, + URL: s.webdavURL(ctx, share), + SharedSecret: share.Token, + } +} + +func (s *service) getWebappProtocol(share *ocm.Share) *ocmd.Webapp { + var b strings.Builder + if err := s.webappTmpl.Execute(&b, share); err != nil { + return nil + } + return &ocmd.Webapp{ + URITemplate: b.String(), + } +} + +func (s *service) getDataTransferProtocol(ctx context.Context, share *ocm.Share) *ocmd.Datatx { + var size uint64 + + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return nil + } + // get the path of the share + statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{ + Ref: &providerpb.Reference{ + ResourceId: share.ResourceId, + }, + }) + if err != nil { + return nil + } + + err = s.walker.Walk(ctx, statRes.GetInfo().GetId(), func(path string, info *providerpb.ResourceInfo, err error) error { + if info.Type == providerpb.ResourceType_RESOURCE_TYPE_FILE { + size += info.Size + } + return nil + }) + if err != nil { + return nil + } + return &ocmd.Datatx{ + SourceURI: s.webdavURL(ctx, share), + Size: size, + } +} + +func (s *service) getProtocols(ctx context.Context, share *ocm.Share) ocmd.Protocols { + var p ocmd.Protocols + for _, m := range share.AccessMethods { + var newProtocol ocmd.Protocol + switch t := m.Term.(type) { + case *ocm.AccessMethod_WebdavOptions: + newProtocol = s.getWebdavProtocol(ctx, share, t) + case *ocm.AccessMethod_WebappOptions: + newProtocol = s.getWebappProtocol(share) + case *ocm.AccessMethod_TransferOptions: + newProtocol = s.getDataTransferProtocol(ctx, share) + } + if newProtocol != nil { + p = append(p, newProtocol) + } + } + return p } -// Note: this is for outgoing OCM shares -// This function is used when you for instance -// call `ocm-share-create` in reva-cli. -// For incoming OCM shares from internal/http/services/ocmd/shares.go -// there is the very similar but slightly different function -// CreateOCMCoreShare (the "Core" somehow means "incoming"). -// So make sure to keep in mind the difference between this file for outgoing: -// internal/grpc/services/ocmshareprovider/ocmshareprovider.go -// and the other one for incoming: -// internal/grpc/service/ocmcore/ocmcore.go -// Both functions end up calling the same s.sm.Share function -// on the OCM share manager: -// pkg/ocm/share/manager/{json|nextcloud|...} func (s *service) CreateOCMShare(ctx context.Context, req *ocm.CreateOCMShareRequest) (*ocm.CreateOCMShareResponse, error) { - if req.Opaque == nil { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "can't find resource permissions"), - }, nil + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + return nil, err + } + statRes, err := gatewayClient.Stat(ctx, &providerpb.StatRequest{ + Ref: &providerpb.Reference{ + ResourceId: req.ResourceId, + }, + }) + if err != nil { + return nil, err } - var permissions string - permOpaque, ok := req.Opaque.Map["permissions"] - if !ok { + if statRes.Status.Code != rpc.Code_CODE_OK { + if statRes.Status.Code == rpc.Code_CODE_NOT_FOUND { + return &ocm.CreateOCMShareResponse{ + Status: status.NewNotFound(ctx, statRes.Status.Message), + }, nil + } return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "resource permissions not set"), + Status: status.NewInternal(ctx, statRes.Status.Message), }, nil } - switch permOpaque.Decoder { - case "plain": - permissions = string(permOpaque.Value) - default: - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "invalid opaque entry decoder"), - }, nil + + info := statRes.Info + user := ctxpkg.ContextMustGetUser(ctx) + tkn := utils.RandString(32) + now := time.Now().UnixNano() + ts := &typespb.Timestamp{ + Seconds: uint64(now / 1000000000), + Nanos: uint32(now % 1000000000), + } + + ocmshare := &ocm.Share{ + Token: tkn, + Name: filepath.Base(info.Path), + ResourceId: req.ResourceId, + Grantee: req.Grantee, + ShareType: ocm.ShareType_SHARE_TYPE_USER, + Owner: info.Owner, + Creator: user.Id, + Ctime: ts, + Mtime: ts, + Expiration: req.Expiration, + AccessMethods: req.AccessMethods, } - var name string - nameOpaque, ok := req.Opaque.Map["name"] - if !ok { + ocmshare, err = s.repo.StoreShare(ctx, ocmshare) + if err != nil { + if errors.Is(err, share.ErrShareAlreadyExisting) { + return &ocm.CreateOCMShareResponse{ + Status: status.NewAlreadyExists(ctx, err, "share already exists"), + }, nil + } return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "resource name not set"), + Status: status.NewInternal(ctx, err.Error()), }, nil } - switch nameOpaque.Decoder { - case "plain": - name = string(nameOpaque.Value) - default: + + ocmEndpoint, err := getOCMEndpoint(req.RecipientMeshProvider) + if err != nil { return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "invalid opaque entry decoder"), + Status: status.NewInvalidArg(ctx, "the selected provider does not have an OCM endpoint"), }, nil } - // discover share type - sharetype := ocm.Share_SHARE_TYPE_REGULAR - // FIXME: https://github.com/cs3org/reva/issues/2402 - protocol, ok := req.Opaque.Map["protocol"] - if ok { - switch protocol.Decoder { - case "plain": - if string(protocol.Value) == "datatx" { - sharetype = ocm.Share_SHARE_TYPE_TRANSFER - } - default: - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "error creating share"), - }, nil - } - // token = protocol FIXME! + newShareReq := &client.NewShareRequest{ + ShareWith: formatOCMUser(req.Grantee.GetUserId()), + Name: ocmshare.Name, + ProviderID: ocmshare.Id.OpaqueId, + Owner: formatOCMUser(&userpb.UserId{ + OpaqueId: info.Owner.OpaqueId, + Idp: s.conf.ProviderDomain, // FIXME: this is not generally true in case of resharing + }), + Sender: formatOCMUser(&userpb.UserId{ + OpaqueId: user.Id.OpaqueId, + Idp: s.conf.ProviderDomain, + }), + SenderDisplayName: user.DisplayName, + ShareType: "user", + ResourceType: getResourceType(info), + Protocols: s.getProtocols(ctx, ocmshare), } - var sharedSecret string - share, err := s.sm.Share(ctx, req.ResourceId, req.Grant, name, req.RecipientMeshProvider, permissions, nil, sharedSecret, sharetype) + if req.Expiration != nil { + newShareReq.Expiration = req.Expiration.Seconds + } + newShareRes, err := s.client.NewShare(ctx, ocmEndpoint, newShareReq) if err != nil { - return &ocm.CreateOCMShareResponse{ - Status: status.NewInternal(ctx, "error creating share"), - }, nil + switch { + case errors.Is(err, client.ErrInvalidParameters): + return &ocm.CreateOCMShareResponse{ + Status: status.NewInvalidArg(ctx, err.Error()), + }, nil + case errors.Is(err, client.ErrServiceNotTrusted): + return &ocm.CreateOCMShareResponse{ + Status: status.NewInvalidArg(ctx, err.Error()), + }, nil + default: + return &ocm.CreateOCMShareResponse{ + Status: status.NewInternal(ctx, err.Error()), + }, nil + } } res := &ocm.CreateOCMShareResponse{ - Status: status.NewOK(ctx), - Share: share, + Status: status.NewOK(ctx), + Share: ocmshare, + RecipientDisplayName: newShareRes.RecipientDisplayName, } return res, nil } func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareRequest) (*ocm.RemoveOCMShareResponse, error) { - err := s.sm.Unshare(ctx, req.Ref) - if err != nil { + // TODO (gdelmont): notify the remote provider using the /notification ocm endpoint + // https://cs3org.github.io/OCM-API/docs.html?branch=develop&repo=OCM-API&user=cs3org#/paths/~1notifications/post + user := ctxpkg.ContextMustGetUser(ctx) + if err := s.repo.DeleteShare(ctx, user, req.Ref); err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.RemoveOCMShareResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } return &ocm.RemoveOCMShareResponse{ Status: status.NewInternal(ctx, "error removing share"), }, nil @@ -202,8 +390,18 @@ func (s *service) RemoveOCMShare(ctx context.Context, req *ocm.RemoveOCMShareReq } func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) (*ocm.GetOCMShareResponse, error) { - share, err := s.sm.GetShare(ctx, req.Ref) + // if the request is by token, the user does not need to be in the ctx + var user *userpb.User + if req.Ref.GetToken() == "" { + user = ctxpkg.ContextMustGetUser(ctx) + } + ocmshare, err := s.repo.GetShare(ctx, user, req.Ref) if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.GetOCMShareResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } return &ocm.GetOCMShareResponse{ Status: status.NewInternal(ctx, "error getting share"), }, nil @@ -211,12 +409,36 @@ func (s *service) GetOCMShare(ctx context.Context, req *ocm.GetOCMShareRequest) return &ocm.GetOCMShareResponse{ Status: status.NewOK(ctx), - Share: share, + Share: ocmshare, + }, nil +} + +func (s *service) GetOCMShareByToken(ctx context.Context, req *ocm.GetOCMShareByTokenRequest) (*ocm.GetOCMShareByTokenResponse, error) { + ocmshare, err := s.repo.GetShare(ctx, nil, &ocm.ShareReference{ + Spec: &ocm.ShareReference_Token{ + Token: req.Token, + }, + }) + if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewInternal(ctx, "error getting share"), + }, nil + } + + return &ocm.GetOCMShareByTokenResponse{ + Status: status.NewOK(ctx), + Share: ocmshare, }, nil } func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesRequest) (*ocm.ListOCMSharesResponse, error) { - shares, err := s.sm.ListShares(ctx, req.Filters) // TODO(labkode): add filter to share manager + user := ctxpkg.ContextMustGetUser(ctx) + shares, err := s.repo.ListShares(ctx, user, req.Filters) if err != nil { return &ocm.ListOCMSharesResponse{ Status: status.NewInternal(ctx, "error listing shares"), @@ -231,8 +453,19 @@ func (s *service) ListOCMShares(ctx context.Context, req *ocm.ListOCMSharesReque } func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareRequest) (*ocm.UpdateOCMShareResponse, error) { - _, err := s.sm.UpdateShare(ctx, req.Ref, req.Field.GetPermissions()) // TODO(labkode): check what to update + user := ctxpkg.ContextMustGetUser(ctx) + if len(req.Field) == 0 { + return &ocm.UpdateOCMShareResponse{ + Status: status.NewOK(ctx), + }, nil + } + _, err := s.repo.UpdateShare(ctx, user, req.Ref, req.Field...) if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.UpdateOCMShareResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } return &ocm.UpdateOCMShareResponse{ Status: status.NewInternal(ctx, "error updating share"), }, nil @@ -245,7 +478,8 @@ func (s *service) UpdateOCMShare(ctx context.Context, req *ocm.UpdateOCMShareReq } func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceivedOCMSharesRequest) (*ocm.ListReceivedOCMSharesResponse, error) { - shares, err := s.sm.ListReceivedShares(ctx) + user := ctxpkg.ContextMustGetUser(ctx) + shares, err := s.repo.ListReceivedShares(ctx, user) if err != nil { return &ocm.ListReceivedOCMSharesResponse{ Status: status.NewInternal(ctx, "error listing received shares"), @@ -260,8 +494,14 @@ func (s *service) ListReceivedOCMShares(ctx context.Context, req *ocm.ListReceiv } func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateReceivedOCMShareRequest) (*ocm.UpdateReceivedOCMShareResponse, error) { - _, err := s.sm.UpdateReceivedShare(ctx, req.Share, req.UpdateMask) // TODO(labkode): check what to update + user := ctxpkg.ContextMustGetUser(ctx) + _, err := s.repo.UpdateReceivedShare(ctx, user, req.Share, req.UpdateMask) if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.UpdateReceivedOCMShareResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } return &ocm.UpdateReceivedOCMShareResponse{ Status: status.NewInternal(ctx, "error updating received share"), }, nil @@ -274,8 +514,14 @@ func (s *service) UpdateReceivedOCMShare(ctx context.Context, req *ocm.UpdateRec } func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedOCMShareRequest) (*ocm.GetReceivedOCMShareResponse, error) { - share, err := s.sm.GetReceivedShare(ctx, req.Ref) + user := ctxpkg.ContextMustGetUser(ctx) + ocmshare, err := s.repo.GetReceivedShare(ctx, user, req.Ref) if err != nil { + if errors.Is(err, share.ErrShareNotFound) { + return &ocm.GetReceivedOCMShareResponse{ + Status: status.NewNotFound(ctx, "share does not exist"), + }, nil + } return &ocm.GetReceivedOCMShareResponse{ Status: status.NewInternal(ctx, "error getting received share"), }, nil @@ -283,7 +529,7 @@ func (s *service) GetReceivedOCMShare(ctx context.Context, req *ocm.GetReceivedO res := &ocm.GetReceivedOCMShareResponse{ Status: status.NewOK(ctx), - Share: share, + Share: ocmshare, } return res, nil } diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index 48fb777919..4704c5e02a 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -30,10 +30,12 @@ import ( userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/pkg/appctx" ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/errtypes" "github.com/cs3org/reva/v2/pkg/rgrpc" "github.com/cs3org/reva/v2/pkg/rgrpc/status" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" @@ -105,13 +107,10 @@ func New(m map[string]interface{}, ss *grpc.Server) (rgrpc.Service, error) { } func (s *service) SetArbitraryMetadata(ctx context.Context, req *provider.SetArbitraryMetadataRequest) (*provider.SetArbitraryMetadataResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.SetArbitraryMetadataResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -127,13 +126,10 @@ func (s *service) UnsetArbitraryMetadata(ctx context.Context, req *provider.Unse // SetLock puts a lock on the given reference func (s *service) SetLock(ctx context.Context, req *provider.SetLockRequest) (*provider.SetLockResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.SetLockResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -145,13 +141,10 @@ func (s *service) SetLock(ctx context.Context, req *provider.SetLockRequest) (*p // GetLock returns an existing lock on the given reference func (s *service) GetLock(ctx context.Context, req *provider.GetLockRequest) (*provider.GetLockResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.GetLockResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -163,13 +156,10 @@ func (s *service) GetLock(ctx context.Context, req *provider.GetLockRequest) (*p // RefreshLock refreshes an existing lock on the given reference func (s *service) RefreshLock(ctx context.Context, req *provider.RefreshLockRequest) (*provider.RefreshLockResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.RefreshLockResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -181,13 +171,10 @@ func (s *service) RefreshLock(ctx context.Context, req *provider.RefreshLockRequ // Unlock removes an existing lock from the given reference func (s *service) Unlock(ctx context.Context, req *provider.UnlockRequest) (*provider.UnlockResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.UnlockResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -220,26 +207,16 @@ func (s *service) InitiateFileDownload(ctx context.Context, req *provider.Initia return s.initiateFileDownload(ctx, req) } -func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, string, *link.PublicShare, *rpc.Status, error) { +func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider.Reference) (*provider.Reference, *provider.ResourceInfo, string, error) { log := appctx.GetLogger(ctx) - share, ok := extractLinkFromScope(ctx) - if !ok { - return nil, "", nil, nil, gstatus.Errorf(codes.NotFound, "share or token not found") - } - - // the share is minimally populated, we need more than the token - // look up complete share - ls, shareInfo, st, err := s.resolveToken(ctx, share.Token) - switch { - case err != nil: - return nil, "", nil, nil, err - case st != nil: - return nil, "", nil, st, nil + info, _, _, token, err := s.extractLinkFromScope(ctx) + if err != nil { + return nil, nil, "", err } var path string - switch shareInfo.Type { + switch info.Type { case provider.ResourceType_RESOURCE_TYPE_CONTAINER: // folders point to the folder -> path needs to be added path = utils.MakeRelativePath(ref.Path) @@ -252,37 +229,34 @@ func (s *service) translatePublicRefToCS3Ref(ctx context.Context, ref *provider. } cs3Ref := &provider.Reference{ - ResourceId: shareInfo.Id, + ResourceId: info.Id, Path: path, } log.Debug(). Interface("sourceRef", ref). Interface("cs3Ref", cs3Ref). - Interface("share", ls). - Str("tkn", share.Token). - Str("originalPath", shareInfo.Path). + Str("tkn", token). + Str("originalPath", info.Path). Str("relativePath", path). Msg("translatePublicRefToCS3Ref") - return cs3Ref, share.Token, ls, nil, nil + return cs3Ref, info, token, nil } func (s *service) initiateFileDownload(ctx context.Context, req *provider.InitiateFileDownloadRequest) (*provider.InitiateFileDownloadResponse, error) { - cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) switch { case err != nil: - return nil, err - case st != nil: return &provider.InitiateFileDownloadResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil - case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileDownload: + case info.PermissionSet == nil || !info.PermissionSet.InitiateFileDownload: return &provider.InitiateFileDownloadResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileDownload permission"), }, nil } dReq := &provider.InitiateFileDownloadRequest{ - Ref: cs3Ref, + Ref: ref, } gatewayClient, err := s.gatewaySelector.Next() @@ -325,15 +299,13 @@ func (s *service) initiateFileDownload(ctx context.Context, req *provider.Initia } func (s *service) InitiateFileUpload(ctx context.Context, req *provider.InitiateFileUploadRequest) (*provider.InitiateFileUploadResponse, error) { - cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) switch { case err != nil: - return nil, err - case st != nil: return &provider.InitiateFileUploadResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil - case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.InitiateFileUpload: + case info.PermissionSet == nil || !info.PermissionSet.InitiateFileUpload: return &provider.InitiateFileUploadResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant InitiateFileUpload permission"), }, nil @@ -443,12 +415,19 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora } } - // if there is no public scope there are no publicstorage spaces - share, ok := extractLinkFromScope(ctx) - if !ok { - return &provider.ListStorageSpacesResponse{ - Status: &rpc.Status{Code: rpc.Code_CODE_OK}, - }, nil + info, _, grantee, token, err := s.extractLinkFromScope(ctx) + if err != nil { + switch err.(type) { + case errtypes.NotFound: + // if there is no public scope there are no publicstorage spaces + return &provider.ListStorageSpacesResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_OK}, + }, nil + default: + return &provider.ListStorageSpacesResponse{ + Status: &rpc.Status{Code: rpc.Code_CODE_INTERNAL}, + }, nil + } } if len(spaceTypes) == 0 { @@ -466,7 +445,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora case "grant": // when a list storage space with the resourceid of an external // resource is made we may have a grant for it - root := share.ResourceId + root := info.Id if spaceID != nil && !utils.ResourceIDEqual(spaceID, root) { // none of our business continue @@ -477,7 +456,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora OpaqueId: storagespace.FormatResourceID(*root), }, SpaceType: "grant", - Owner: &userv1beta1.User{Id: share.Owner}, + Owner: &userv1beta1.User{Id: grantee}, // the publicstorageprovider keeps track of mount points Root: root, } @@ -487,15 +466,15 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora root := &provider.ResourceId{ StorageId: utils.PublicStorageProviderID, SpaceId: utils.PublicStorageSpaceID, - OpaqueId: share.Token, // the link share has no id, only the token + OpaqueId: token, // the link share has no id, only the token } if spaceID != nil { switch { case utils.ResourceIDEqual(spaceID, root): // we have a virtual node - case utils.ResourceIDEqual(spaceID, share.ResourceId): + case utils.ResourceIDEqual(spaceID, info.Id): // we have a mount point - root = share.ResourceId + root = info.Id default: // none of our business continue @@ -506,7 +485,7 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora OpaqueId: storagespace.FormatResourceID(*root), }, SpaceType: "mountpoint", - Owner: &userv1beta1.User{Id: share.Owner}, // FIXME actually, the mount point belongs to no one? + Owner: &userv1beta1.User{Id: grantee}, // FIXME actually, the mount point belongs to no one? // the publicstorageprovider keeps track of mount points Root: root, } @@ -517,27 +496,44 @@ func (s *service) ListStorageSpaces(ctx context.Context, req *provider.ListStora return res, nil } -func extractLinkFromScope(ctx context.Context) (*link.PublicShare, bool) { +func (s *service) extractLinkFromScope(ctx context.Context) (*provider.ResourceInfo, interface{}, *userv1beta1.UserId, string, error) { scopes, ok := ctxpkg.ContextGetScopes(ctx) if !ok { - return nil, false + return nil, nil, nil, "", errtypes.NotFound("No scopes found in context") } - var share *link.PublicShare for k, v := range scopes { - if strings.HasPrefix(k, "publicshare:") && v.Resource.Decoder == "json" { - share = &link.PublicShare{} + if strings.HasPrefix(k, "ocmshare:") && v.Resource.Decoder == "json" { + share := &ocm.Share{} err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, share) if err != nil { - continue + return nil, nil, nil, "", errtypes.InternalError("failed to unmarshal public share") + } + + // the share is minimally populated, we need more than the token + // look up complete share + info, resolvedShare, err := s.resolveToken(ctx, share) + if err != nil { + return nil, nil, nil, "", err } + return info, resolvedShare, share.Owner, share.Token, nil + } else if strings.HasPrefix(k, "publicshare:") && v.Resource.Decoder == "json" { + share := &link.PublicShare{} + err := utils.UnmarshalJSONToProtoV1(v.Resource.Value, share) + if err != nil { + return nil, nil, nil, "", errtypes.InternalError("failed to unmarshal public share") + } + + // the share is minimally populated, we need more than the token + // look up complete share + info, resolvedShare, err := s.resolveToken(ctx, share) + if err != nil { + return nil, nil, nil, "", err + } + return info, resolvedShare, share.Owner, share.Token, nil } } - if share == nil { - return nil, false - } - return share, true + return nil, nil, nil, "", errtypes.NotFound("No public storage info found in scopes") } - func (s *service) UpdateStorageSpace(ctx context.Context, req *provider.UpdateStorageSpaceRequest) (*provider.UpdateStorageSpaceResponse, error) { return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") } @@ -555,15 +551,13 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta Value: attribute.StringValue(req.Ref.String()), }) - cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) switch { case err != nil: - return nil, err - case st != nil: return &provider.CreateContainerResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil - case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.CreateContainer: + case info.PermissionSet == nil || !info.PermissionSet.CreateContainer: return &provider.CreateContainerResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant CreateContainer permission"), }, nil @@ -591,13 +585,10 @@ func (s *service) CreateContainer(ctx context.Context, req *provider.CreateConta } func (s *service) TouchFile(ctx context.Context, req *provider.TouchFileRequest) (*provider.TouchFileResponse, error) { - ref, _, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) - switch { - case err != nil: - return nil, err - case st != nil: + ref, _, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + if err != nil { return &provider.TouchFileResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil } gatewayClient, err := s.gatewaySelector.Next() @@ -616,15 +607,13 @@ func (s *service) Delete(ctx context.Context, req *provider.DeleteRequest) (*pro Value: attribute.StringValue(req.Ref.String()), }) - cs3Ref, _, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) + cs3Ref, info, _, err := s.translatePublicRefToCS3Ref(ctx, req.Ref) switch { case err != nil: - return nil, err - case st != nil: return &provider.DeleteResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve reference", err), }, nil - case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Delete: + case info.PermissionSet == nil || !info.PermissionSet.Delete: return &provider.DeleteResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant Delete permission"), }, nil @@ -666,27 +655,22 @@ func (s *service) Move(ctx context.Context, req *provider.MoveRequest) (*provide }, ) - cs3RefSource, tknSource, ls, st, err := s.translatePublicRefToCS3Ref(ctx, req.Source) + cs3RefSource, info, tknSource, err := s.translatePublicRefToCS3Ref(ctx, req.Source) switch { case err != nil: - return nil, err - case st != nil: return &provider.MoveResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve source reference", err), }, nil - case ls.GetPermissions() == nil || !ls.GetPermissions().Permissions.Move: + case info.PermissionSet == nil || !info.PermissionSet.Move: return &provider.MoveResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant Move permission"), }, nil } // FIXME: maybe there's a shortcut possible here using the source path - cs3RefDestination, tknDest, _, st, err := s.translatePublicRefToCS3Ref(ctx, req.Destination) - switch { - case err != nil: - return nil, err - case st != nil: + cs3RefDestination, _, tknDest, err := s.translatePublicRefToCS3Ref(ctx, req.Destination) + if err != nil { return &provider.MoveResponse{ - Status: st, + Status: status.NewStatusFromErrType(ctx, "failed to resolve destination reference", err), }, nil } @@ -728,40 +712,31 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide Value: attribute.StringValue(req.Ref.String()), }) - share, ok := extractLinkFromScope(ctx) - if !ok { - return &provider.StatResponse{ - Status: status.NewNotFound(ctx, "share or token not found"), - }, nil - } - - // the share is minimally populated, we need more than the token - // look up complete share - share, shareInfo, st, err := s.resolveToken(ctx, share.Token) - switch { - case err != nil: - return nil, err - case st != nil: - return &provider.StatResponse{ - Status: st, - }, nil - case share.GetPermissions() == nil || !share.GetPermissions().Permissions.Stat: - return &provider.StatResponse{ - Status: status.NewPermissionDenied(ctx, nil, "share does not grant Stat permission"), - }, nil + info, share, _, token, err := s.extractLinkFromScope(ctx) + if err != nil { + switch err.(type) { + case errtypes.NotFound: + return &provider.StatResponse{ + Status: status.NewNotFound(ctx, "share or token not found"), + }, nil + default: + return &provider.StatResponse{ + Status: status.NewInternal(ctx, "share or token not found"), + }, nil + } } - if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE || req.Ref.Path == "" { + if info.Type == provider.ResourceType_RESOURCE_TYPE_FILE || req.Ref.Path == "" { res := &provider.StatResponse{ Status: status.NewOK(ctx), - Info: shareInfo, + Info: info, } - s.augmentStatResponse(ctx, res, shareInfo, share, share.Token) + s.augmentStatResponse(ctx, res.Info, info, share, token) return res, nil } ref := &provider.Reference{ - ResourceId: share.ResourceId, + ResourceId: info.Id, Path: utils.MakeRelativePath(req.Ref.Path), } @@ -776,38 +751,38 @@ func (s *service) Stat(ctx context.Context, req *provider.StatRequest) (*provide }, nil } - s.augmentStatResponse(ctx, statResponse, shareInfo, share, share.Token) + s.augmentStatResponse(ctx, statResponse.Info, info, share, token) return statResponse, nil } -func (s *service) augmentStatResponse(ctx context.Context, res *provider.StatResponse, shareInfo *provider.ResourceInfo, share *link.PublicShare, tkn string) { +func (s *service) augmentStatResponse(ctx context.Context, statInfo *provider.ResourceInfo, shareInfo *provider.ResourceInfo, share interface{}, tkn string) { // prevent leaking internal paths - if res.Info != nil { - if err := addShare(res.Info, share); err != nil { - appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", res.Info).Msg("error when adding share") + if statInfo != nil { + if err := addShare(statInfo, share); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", statInfo).Msg("error when adding share") } var sharePath string if shareInfo.Type == provider.ResourceType_RESOURCE_TYPE_FILE { sharePath = path.Base(shareInfo.Path) } else { - sharePath = strings.TrimPrefix(res.Info.Path, shareInfo.Path) + sharePath = strings.TrimPrefix(statInfo.Path, shareInfo.Path) } - res.Info.Path = path.Join("/", sharePath) - filterPermissions(res.Info.PermissionSet, share.GetPermissions().Permissions) + statInfo.Path = path.Join("/", sharePath) + filterPermissions(statInfo.PermissionSet, shareInfo.PermissionSet) } } -func addShare(i *provider.ResourceInfo, ls *link.PublicShare) error { +func addShare(i *provider.ResourceInfo, share interface{}) error { if i.Opaque == nil { i.Opaque = &typesv1beta1.Opaque{} } if i.Opaque.Map == nil { i.Opaque.Map = map[string]*typesv1beta1.OpaqueEntry{} } - val, err := json.Marshal(ls) + val, err := json.Marshal(share) if err != nil { return err } @@ -820,25 +795,20 @@ func (s *service) ListContainerStream(req *provider.ListContainerStreamRequest, } func (s *service) ListContainer(ctx context.Context, req *provider.ListContainerRequest) (*provider.ListContainerResponse, error) { - - share, ok := extractLinkFromScope(ctx) - if !ok { - return &provider.ListContainerResponse{ - Status: status.NewNotFound(ctx, "share or token not found"), - }, nil - } - // the share is minimally populated, we need more than the token - // look up complete share - share, _, st, err := s.resolveToken(ctx, share.Token) - switch { - case err != nil: - return nil, err - case st != nil: - return &provider.ListContainerResponse{ - Status: st, - }, nil + info, share, _, _, err := s.extractLinkFromScope(ctx) + if err != nil { + switch err.(type) { + case errtypes.NotFound: + return &provider.ListContainerResponse{ + Status: status.NewNotFound(ctx, "share or token not found"), + }, nil + default: + return &provider.ListContainerResponse{ + Status: status.NewInternal(ctx, "share or token not found"), + }, nil + } } - if share.GetPermissions() == nil || !share.GetPermissions().Permissions.ListContainer { + if info.PermissionSet == nil || !info.PermissionSet.ListContainer { return &provider.ListContainerResponse{ Status: status.NewPermissionDenied(ctx, nil, "share does not grant ListContainer permission"), }, nil @@ -852,7 +822,7 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer ctx, &provider.ListContainerRequest{ Ref: &provider.Reference{ - ResourceId: share.ResourceId, + ResourceId: info.Id, // prefix relative path with './' to make it a CS3 relative path Path: utils.MakeRelativePath(req.Ref.Path), }, @@ -867,7 +837,7 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer for i := range listContainerR.Infos { // FIXME how do we reduce permissions to what is granted by the public link? // only a problem for id based access -> middleware - filterPermissions(listContainerR.Infos[i].PermissionSet, share.GetPermissions().Permissions) + filterPermissions(listContainerR.Infos[i].PermissionSet, info.PermissionSet) if err := addShare(listContainerR.Infos[i], share); err != nil { appctx.GetLogger(ctx).Error().Err(err).Interface("share", share).Interface("info", listContainerR.Infos[i]).Msg("error when adding share") } @@ -953,45 +923,70 @@ func (s *service) GetQuota(ctx context.Context, req *provider.GetQuotaRequest) ( return nil, gstatus.Errorf(codes.Unimplemented, "method not implemented") } -// resolveToken returns the path and share for the publicly shared resource. -func (s *service) resolveToken(ctx context.Context, token string) (*link.PublicShare, *provider.ResourceInfo, *rpc.Status, error) { - driver, err := pool.GetGatewayServiceClient(s.conf.GatewayAddr) +// resolveToken returns the resource info for the publicly shared resource. +func (s *service) resolveToken(ctx context.Context, share interface{}) (*provider.ResourceInfo, interface{}, error) { + gatewayClient, err := s.gatewaySelector.Next() if err != nil { - return nil, nil, nil, err - } - - publicShareResponse, err := driver.GetPublicShare( - ctx, - &link.GetPublicShareRequest{ - Ref: &link.PublicShareReference{ - Spec: &link.PublicShareReference_Token{ - Token: token, + return nil, nil, err + } + + resourceID := &provider.ResourceId{} + perms := &provider.ResourcePermissions{} + var resolvedShare interface{} + switch v := share.(type) { + case *link.PublicShare: + publicShareResponse, err := gatewayClient.GetPublicShare( + ctx, + &link.GetPublicShareRequest{ + Ref: &link.PublicShareReference{ + Spec: &link.PublicShareReference_Token{ + Token: v.Token, + }, }, + Sign: true, }, - Sign: true, - }, - ) - switch { - case err != nil: - return nil, nil, nil, err - case publicShareResponse.Status.Code != rpc.Code_CODE_OK: - return nil, nil, publicShareResponse.Status, nil + ) + switch { + case err != nil: + return nil, nil, err + case publicShareResponse.Status.Code != rpc.Code_CODE_OK: + return nil, nil, errtypes.NewErrtypeFromStatus(publicShareResponse.Status) + } + resolvedShare = publicShareResponse.GetShare() + resourceID = publicShareResponse.GetShare().GetResourceId() + perms = publicShareResponse.GetShare().GetPermissions().GetPermissions() + case *ocm.Share: + gsr, err := gatewayClient.GetOCMShareByToken(ctx, &ocm.GetOCMShareByTokenRequest{ + Token: v.Token, + }) + switch { + case err != nil: + return nil, nil, err + case gsr.Status.Code != rpc.Code_CODE_OK: + return nil, nil, errtypes.NewErrtypeFromStatus(gsr.Status) + } + accessMethods := gsr.GetShare().GetAccessMethods() + if len(accessMethods) == 0 { + return nil, nil, errtypes.PermissionDenied("failed to get access to the requested resource") + } + resolvedShare = gsr.GetShare() + resourceID = gsr.GetShare().GetResourceId() + perms = accessMethods[0].GetWebdavOptions().Permissions } - gatewayClient, err := s.gatewaySelector.Next() - if err != nil { - return nil, nil, nil, err - } sRes, err := gatewayClient.Stat(ctx, &provider.StatRequest{ Ref: &provider.Reference{ - ResourceId: publicShareResponse.GetShare().GetResourceId(), + ResourceId: resourceID, }, }) switch { case err != nil: - return nil, nil, nil, err + return nil, nil, err case sRes.Status.Code != rpc.Code_CODE_OK: - return nil, nil, sRes.Status, nil + return nil, nil, errtypes.NewErrtypeFromStatus(sRes.Status) } - return publicShareResponse.GetShare(), sRes.Info, nil, nil + + // Set permissions + sRes.Info.PermissionSet = perms + return sRes.Info, resolvedShare, nil } diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 800fe8408e..c830570d8c 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -27,6 +27,7 @@ import ( "time" "github.com/bluele/gcache" + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" @@ -69,7 +70,7 @@ type config struct { CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"` CredentialChain []string `mapstructure:"credential_chain"` CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"` - TokenStrategy string `mapstructure:"token_strategy"` + TokenStrategyChain []string `mapstructure:"token_strategy_chain"` TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"` TokenManager string `mapstructure:"token_manager"` TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` @@ -97,8 +98,8 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider conf.GatewaySvc = sharedconf.GetGatewaySVC(conf.GatewaySvc) // set defaults - if conf.TokenStrategy == "" { - conf.TokenStrategy = "header" + if len(conf.TokenStrategyChain) == 0 { + conf.TokenStrategyChain = []string{"header"} } if conf.TokenWriter == "" { @@ -135,19 +136,22 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider credChain[key] = credStrategy } - g, ok := tokenregistry.NewTokenFuncs[conf.TokenStrategy] - if !ok { - return nil, fmt.Errorf("token strategy not found: %s", conf.TokenStrategy) - } - - tokenStrategy, err := g(conf.TokenStrategies[conf.TokenStrategy]) - if err != nil { - return nil, err + tokenStrategyChain := make([]auth.TokenStrategy, 0, len(conf.TokenStrategyChain)) + for _, strategy := range conf.TokenStrategyChain { + g, ok := tokenregistry.NewTokenFuncs[strategy] + if !ok { + return nil, fmt.Errorf("token strategy not found: %s", strategy) + } + tokenStrategy, err := g(conf.TokenStrategies[strategy]) + if err != nil { + return nil, err + } + tokenStrategyChain = append(tokenStrategyChain, tokenStrategy) } h, ok := tokenmgr.NewFuncs[conf.TokenManager] if !ok { - return nil, fmt.Errorf("token manager not found: %s", conf.TokenStrategy) + return nil, fmt.Errorf("token manager not found: %s", conf.TokenManager) } tokenManager, err := h(conf.TokenManagers[conf.TokenManager]) @@ -192,7 +196,7 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider isUnprotectedEndpoint = true } - ctx, err := authenticateUser(w, r, conf, tokenStrategy, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint) + ctx, err := authenticateUser(w, r, conf, tokenStrategyChain, tokenManager, tokenWriter, credChain, isUnprotectedEndpoint) if err != nil { if !isUnprotectedEndpoint { return @@ -212,7 +216,7 @@ func New(m map[string]interface{}, unprotected []string, tp trace.TracerProvider return chain, nil } -func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategy auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) { +func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, tokenStrategies []auth.TokenStrategy, tokenManager token.Manager, tokenWriter auth.TokenWriter, credChain map[string]auth.CredentialStrategy, isUnprotectedEndpoint bool) (context.Context, error) { ctx := r.Context() log := appctx.GetLogger(ctx) @@ -225,71 +229,85 @@ func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, toke return nil, err } - tkn := tokenStrategy.GetToken(r) - if tkn == "" { - log.Warn().Msg("core access token not set") + // reva token or auth token can be passed using the same technique (for example bearer) + // before validating it against an auth provider, we can check directly if it's a reva + // token and if not try to use it for authenticating the user. + for _, tokenStrategy := range tokenStrategies { + token := tokenStrategy.GetToken(r) + if token != "" { + if user, tokenScope, ok := isTokenValid(r, tokenManager, token); ok { + if err := insertGroupsInUser(ctx, userGroupsCache, client, user); err != nil { + logError(isUnprotectedEndpoint, log, err, "got an error retrieving groups for user "+user.Username, http.StatusInternalServerError, w) + return nil, err + } + return ctxWithUserInfo(ctx, r, user, token, tokenScope), nil + } + } + } - userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain) + log.Warn().Msg("core access token not set") - // obtain credentials (basic auth, bearer token, ...) based on user agent - var creds *auth.Credentials - for _, k := range userAgentCredKeys { - creds, err = credChain[k].GetCredentials(w, r) - if err != nil { - log.Debug().Err(err).Msg("error retrieving credentials") - } + userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain) - if creds != nil { - log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID) - break - } + // obtain credentials (basic auth, bearer token, ...) based on user agent + var creds *auth.Credentials + for _, k := range userAgentCredKeys { + creds, err = credChain[k].GetCredentials(w, r) + if err != nil { + log.Debug().Err(err).Msg("error retrieving credentials") } - // if no credentials are found, reply with authentication challenge depending on user agent - if creds == nil { - if !isUnprotectedEndpoint { - for _, key := range userAgentCredKeys { - if cred, ok := credChain[key]; ok { - cred.AddWWWAuthenticate(w, r, conf.Realm) - } else { - panic("auth credential strategy: " + key + "must have been loaded in init method") - } - } - w.WriteHeader(http.StatusUnauthorized) - } - return nil, errtypes.PermissionDenied("no credentials found") + if creds != nil { + log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID) + break } + } - req := &gateway.AuthenticateRequest{ - Type: creds.Type, - ClientId: creds.ClientID, - ClientSecret: creds.ClientSecret, + // if no credentials are found, reply with authentication challenge depending on user agent + if creds == nil { + if !isUnprotectedEndpoint { + for _, key := range userAgentCredKeys { + if cred, ok := credChain[key]; ok { + cred.AddWWWAuthenticate(w, r, conf.Realm) + } else { + log.Error().Msg("auth credential strategy: " + key + "must have been loaded in init method") + w.WriteHeader(http.StatusInternalServerError) + return nil, errtypes.InternalError("no credentials found") + } + } + w.WriteHeader(http.StatusUnauthorized) } + return nil, errtypes.PermissionDenied("no credentials found") + } - log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc) + req := &gateway.AuthenticateRequest{ + Type: creds.Type, + ClientId: creds.ClientID, + ClientSecret: creds.ClientSecret, + } - res, err := client.Authenticate(ctx, req) - if err != nil { - logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w) - return nil, err - } + log.Debug().Msgf("AuthenticateRequest: type: %s, client_id: %s against %s", req.Type, req.ClientId, conf.GatewaySvc) - if res.Status.Code != rpc.Code_CODE_OK { - err := status.NewErrorFromCode(res.Status.Code, "auth") - logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w) - return nil, err - } + res, err := client.Authenticate(ctx, req) + if err != nil { + logError(isUnprotectedEndpoint, log, err, "error calling Authenticate", http.StatusUnauthorized, w) + return nil, err + } - log.Info().Msg("core access token generated") - // write token to response - tkn = res.Token - tokenWriter.WriteToken(tkn, w) - } else { - log.Debug().Msg("access token is already provided") + if res.Status.Code != rpc.Code_CODE_OK { + err := status.NewErrorFromCode(res.Status.Code, "auth") + logError(isUnprotectedEndpoint, log, err, "error generating access token from credentials", http.StatusUnauthorized, w) + return nil, err } + log.Info().Msg("core access token generated") // write token to response + + // write token to response + token := res.Token + tokenWriter.WriteToken(token, w) + // validate token - u, tokenScope, err := tokenManager.DismantleToken(r.Context(), tkn) + u, tokenScope, err := tokenManager.DismantleToken(r.Context(), token) if err != nil { logError(isUnprotectedEndpoint, log, err, "error dismantling token", http.StatusUnauthorized, w) return nil, err @@ -325,15 +343,59 @@ func authenticateUser(w http.ResponseWriter, r *http.Request, conf *config, toke // store user and core access token in context. ctx = ctxpkg.ContextSetUser(ctx, u) - ctx = ctxpkg.ContextSetToken(ctx, tkn) - ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, tkn) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials? + ctx = ctxpkg.ContextSetToken(ctx, token) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) // TODO(jfd): hardcoded metadata key. use PerRPCCredentials? ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) // store scopes in context ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) - return ctx, nil + return ctxWithUserInfo(ctx, r, u, token, tokenScope), nil +} + +func ctxWithUserInfo(ctx context.Context, r *http.Request, user *userpb.User, token string, tokenScope map[string]*authpb.Scope) context.Context { + ctx = ctxpkg.ContextSetUser(ctx, user) + ctx = ctxpkg.ContextSetToken(ctx, token) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, token) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.UserAgentHeader, r.UserAgent()) + ctx = ctxpkg.ContextSetScopes(ctx, tokenScope) + return ctx +} + +func insertGroupsInUser(ctx context.Context, userGroupsCache gcache.Cache, client gateway.GatewayAPIClient, user *userpb.User) error { + if sharedconf.SkipUserGroupsInToken() { + var groups []string + if groupsIf, err := userGroupsCache.Get(user.Id.OpaqueId); err == nil { + groups = groupsIf.([]string) + } else { + groupsRes, err := client.GetUserGroups(ctx, &userpb.GetUserGroupsRequest{UserId: user.Id}) + if err != nil { + return err + } + groups = groupsRes.Groups + _ = userGroupsCache.SetWithExpire(user.Id.OpaqueId, groupsRes.Groups, 3600*time.Second) + } + user.Groups = groups + } + return nil +} + +func isTokenValid(r *http.Request, tokenManager token.Manager, token string) (*userpb.User, map[string]*authpb.Scope, bool) { + ctx := r.Context() + + u, tokenScope, err := tokenManager.DismantleToken(ctx, token) + if err != nil { + return nil, nil, false + } + + // ensure access to the resource is allowed + ok, err := scope.VerifyScope(ctx, tokenScope, r.URL.Path) + if err != nil { + return nil, nil, false + } + + return u, tokenScope, ok } func logError(isUnprotectedEndpoint bool, log *zerolog.Logger, err error, msg string, status int, w http.ResponseWriter) { diff --git a/internal/http/interceptors/auth/credential/loader/loader.go b/internal/http/interceptors/auth/credential/loader/loader.go index dd1bbf328d..52761fbc6a 100644 --- a/internal/http/interceptors/auth/credential/loader/loader.go +++ b/internal/http/interceptors/auth/credential/loader/loader.go @@ -22,5 +22,6 @@ import ( // Load core authentication strategies. _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/basic" _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/bearer" + _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/strategy/ocmshares" // Add your own here. ) diff --git a/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go new file mode 100644 index 0000000000..4e7b26c98e --- /dev/null +++ b/internal/http/interceptors/auth/credential/strategy/ocmshares/ocmshares.go @@ -0,0 +1,58 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmshares + +import ( + "fmt" + "net/http" + + "github.com/cs3org/reva/v2/internal/http/interceptors/auth/credential/registry" + "github.com/cs3org/reva/v2/pkg/auth" +) + +func init() { + registry.Register("ocmshares", New) +} + +const ( + headerShareToken = "ocm-token" +) + +type strategy struct{} + +// New returns a new auth strategy that handles public share verification. +func New(m map[string]interface{}) (auth.CredentialStrategy, error) { + return &strategy{}, nil +} + +func (s *strategy) GetCredentials(w http.ResponseWriter, r *http.Request) (*auth.Credentials, error) { + token := r.Header.Get(headerShareToken) + if token == "" { + token = r.URL.Query().Get(headerShareToken) + } + if token == "" { + return nil, fmt.Errorf("no ocm token provided") + } + + return &auth.Credentials{Type: "ocmshares", ClientID: token}, nil +} + +func (s *strategy) AddWWWAuthenticate(w http.ResponseWriter, r *http.Request, realm string) { + // TODO read realm from forwarded header? +} diff --git a/internal/http/interceptors/auth/token/loader/loader.go b/internal/http/interceptors/auth/token/loader/loader.go index ff03781f58..2c1b4590bf 100644 --- a/internal/http/interceptors/auth/token/loader/loader.go +++ b/internal/http/interceptors/auth/token/loader/loader.go @@ -20,6 +20,7 @@ package loader import ( // Load core token strategies. + _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/strategy/bearer" _ "github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/strategy/header" // Add your own here. ) diff --git a/internal/http/interceptors/auth/token/strategy/bearer/bearer.go b/internal/http/interceptors/auth/token/strategy/bearer/bearer.go new file mode 100644 index 0000000000..4b0a8fc227 --- /dev/null +++ b/internal/http/interceptors/auth/token/strategy/bearer/bearer.go @@ -0,0 +1,84 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package header + +import ( + "mime" + "net/http" + "strings" + + "github.com/cs3org/reva/v2/internal/http/interceptors/auth/token/registry" + "github.com/cs3org/reva/v2/pkg/auth" +) + +func init() { + registry.Register("bearer", New) +} + +type b struct{} + +// New returns a new auth strategy that checks for bearer auth. +func New(m map[string]interface{}) (auth.TokenStrategy, error) { + return b{}, nil +} + +func (b) GetToken(r *http.Request) string { + // Authorization Request Header Field: https://www.rfc-editor.org/rfc/rfc6750#section-2.1 + if tkn, ok := getFromAuthorizationHeader(r); ok { + return tkn + } + + // Form-Encoded Body Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.2 + if tkn, ok := getFromBody(r); ok { + return tkn + } + + // URI Query Parameter: https://www.rfc-editor.org/rfc/rfc6750#section-2.3 + if tkn, ok := getFromQueryParam(r); ok { + return tkn + } + + return "" +} + +func getFromAuthorizationHeader(r *http.Request) (string, bool) { + auth := r.Header.Get("Authorization") + tkn := strings.TrimPrefix(auth, "Bearer ") + return tkn, tkn != "" +} + +func getFromBody(r *http.Request) (string, bool) { + mediatype, _, err := mime.ParseMediaType(r.Header.Get("content-type")) + if err != nil { + return "", false + } + if mediatype != "application/x-www-form-urlencoded" { + return "", false + } + if err = r.ParseForm(); err != nil { + return "", false + } + tkn := r.Form.Get("access-token") + return tkn, tkn != "" +} + +func getFromQueryParam(r *http.Request) (string, bool) { + tkn := r.URL.Query().Get("access_token") + return tkn, tkn != "" +} diff --git a/internal/http/services/datagateway/datagateway.go b/internal/http/services/datagateway/datagateway.go index 69fe36f5ea..2a571d2cf4 100644 --- a/internal/http/services/datagateway/datagateway.go +++ b/internal/http/services/datagateway/datagateway.go @@ -44,7 +44,7 @@ import ( var tracer trace.Tracer func init() { - tracer = otel.Tracer("github.com/cs3org/reva/pkg/storage/utils/decomposedfs/tree") + tracer = otel.Tracer("github.com/cs3org/reva/v2/pkg/storage/utils/decomposedfs/tree") } const ( @@ -324,6 +324,7 @@ func (s *svc) doPut(w http.ResponseWriter, r *http.Request) { return } httpReq.Header = r.Header + httpReq.ContentLength = r.ContentLength httpRes, err := httpClient.Do(httpReq) if err != nil { diff --git a/internal/http/services/loader/loader.go b/internal/http/services/loader/loader.go index b4a963e17f..4368eaec86 100644 --- a/internal/http/services/loader/loader.go +++ b/internal/http/services/loader/loader.go @@ -26,7 +26,6 @@ import ( _ "github.com/cs3org/reva/v2/internal/http/services/dataprovider" _ "github.com/cs3org/reva/v2/internal/http/services/helloworld" _ "github.com/cs3org/reva/v2/internal/http/services/mentix" - _ "github.com/cs3org/reva/v2/internal/http/services/meshdirectory" _ "github.com/cs3org/reva/v2/internal/http/services/metrics" _ "github.com/cs3org/reva/v2/internal/http/services/ocmd" _ "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocdav" @@ -34,6 +33,7 @@ import ( _ "github.com/cs3org/reva/v2/internal/http/services/preferences" _ "github.com/cs3org/reva/v2/internal/http/services/prometheus" _ "github.com/cs3org/reva/v2/internal/http/services/reverseproxy" + _ "github.com/cs3org/reva/v2/internal/http/services/sciencemesh" _ "github.com/cs3org/reva/v2/internal/http/services/siteacc" _ "github.com/cs3org/reva/v2/internal/http/services/sysinfo" _ "github.com/cs3org/reva/v2/internal/http/services/wellknown" diff --git a/internal/http/services/meshdirectory/meshdirectory.go b/internal/http/services/meshdirectory/meshdirectory.go deleted file mode 100644 index 120e7f2c52..0000000000 --- a/internal/http/services/meshdirectory/meshdirectory.go +++ /dev/null @@ -1,155 +0,0 @@ -// 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 meshdirectory - -import ( - "encoding/json" - "fmt" - "net/http" - - meshdirectoryweb "github.com/sciencemesh/meshdirectory-web" - - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - - "github.com/cs3org/reva/v2/internal/http/services/ocmd" - "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/v2/pkg/rhttp/router" - "github.com/cs3org/reva/v2/pkg/sharedconf" - "github.com/pkg/errors" - "github.com/rs/zerolog" - - "github.com/cs3org/reva/v2/pkg/rhttp/global" - "github.com/mitchellh/mapstructure" -) - -func init() { - global.Register("meshdirectory", New) -} - -type config struct { - Prefix string `mapstructure:"prefix"` - GatewaySvc string `mapstructure:"gatewaysvc"` -} - -func (c *config) init() { - c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) - - if c.Prefix == "" { - c.Prefix = "meshdir" - } -} - -type svc struct { - conf *config -} - -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 a new Mesh Directory HTTP service -func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - c, err := parseConfig(m) - if err != nil { - return nil, err - } - - c.init() - - service := &svc{ - conf: c, - } - return service, nil -} - -// Service prefix -func (s *svc) Prefix() string { - return s.conf.Prefix -} - -// Unprotected endpoints -func (s *svc) Unprotected() []string { - return []string{"/"} -} - -// Close performs cleanup. -func (s *svc) Close() error { - return nil -} - -func (s *svc) getClient() (gateway.GatewayAPIClient, error) { - return pool.GetGatewayServiceClient(s.conf.GatewaySvc) -} - -func (s *svc) serveJSON(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - - ctx := r.Context() - - gatewayClient, err := s.getClient() - if err != nil { - ocmd.WriteError(w, r, ocmd.APIErrorServerError, - fmt.Sprintf("error getting grpc client on addr: %v", s.conf.GatewaySvc), err) - return - } - - providers, err := gatewayClient.ListAllProviders(ctx, &providerv1beta1.ListAllProvidersRequest{}) - if err != nil { - ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error listing all providers", err) - return - } - - jsonResponse, err := json.Marshal(providers.Providers) - if err != nil { - ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error marshalling providers data", err) - return - } - - // Write response - _, err = w.Write(jsonResponse) - if err != nil { - ocmd.WriteError(w, r, ocmd.APIErrorServerError, "error writing providers data", err) - return - } - - w.WriteHeader(http.StatusOK) -} - -// HTTP service handler -func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - var head string - head, r.URL.Path = router.ShiftPath(r.URL.Path) - switch head { - case "providers": - s.serveJSON(w, r) - return - default: - r.URL.Path = head + r.URL.Path - meshdirectoryweb.ServeMeshDirectorySPA(w, r) - return - } - }) -} diff --git a/internal/http/services/ocmd/config.go b/internal/http/services/ocmd/config.go deleted file mode 100644 index 8d7b0b419a..0000000000 --- a/internal/http/services/ocmd/config.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2018-2021 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package ocmd - -import ( - "encoding/json" - "fmt" - "net/http" - - "github.com/cs3org/reva/v2/pkg/appctx" -) - -type configData struct { - Enabled bool `json:"enabled" xml:"enabled"` - APIVersion string `json:"apiVersion" xml:"apiVersion"` - Host string `json:"host" xml:"host"` - Endpoint string `json:"endPoint" xml:"endPoint"` - Provider string `json:"provider" xml:"provider"` - ResourceTypes []resourceTypes `json:"resourceTypes" xml:"resourceTypes"` -} - -type resourceTypes struct { - Name string `json:"name"` - ShareTypes []string `json:"shareTypes"` - Protocols resourceTypesProtocols `json:"protocols"` -} - -type resourceTypesProtocols struct { - Webdav string `json:"webdav"` -} - -type configHandler struct { - c configData -} - -func (h *configHandler) init(c *Config) { - h.c = c.Config - if h.c.APIVersion == "" { - h.c.APIVersion = "1.0-proposal1" - } - if h.c.Host == "" { - h.c.Host = "localhost" - } - if h.c.Provider == "" { - h.c.Provider = "cernbox" - } - h.c.Enabled = true - if len(c.Prefix) > 0 { - h.c.Endpoint = fmt.Sprintf("https://%s/%s", h.c.Host, c.Prefix) - } else { - h.c.Endpoint = fmt.Sprintf("https://%s", h.c.Host) - } - h.c.ResourceTypes = []resourceTypes{{ - Name: "file", - ShareTypes: []string{"user"}, - Protocols: resourceTypesProtocols{ - Webdav: fmt.Sprintf("/%s/ocm_webdav", h.c.Provider), - }, - }} -} - -func (h *configHandler) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - indentedConf, _ := json.MarshalIndent(h.c, "", " ") - if _, err := w.Write(indentedConf); err != nil { - log.Err(err).Msg("Error writing to ResponseWriter") - } - - }) -} diff --git a/internal/http/services/ocmd/invites.go b/internal/http/services/ocmd/invites.go index 6f15c1ee6a..439d40a3c9 100644 --- a/internal/http/services/ocmd/invites.go +++ b/internal/http/services/ocmd/invites.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,323 +22,158 @@ import ( "encoding/json" "errors" "fmt" - "io" "mime" "net/http" - "net/url" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/reqres" "github.com/cs3org/reva/v2/pkg/appctx" - ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" - "github.com/cs3org/reva/v2/pkg/rhttp/router" - "github.com/cs3org/reva/v2/pkg/smtpclient" "github.com/cs3org/reva/v2/pkg/utils" ) type invitesHandler struct { - smtpCredentials *smtpclient.SMTPCredentials - gatewayAddr string - meshDirectoryURL string + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] } -func (h *invitesHandler) init(c *Config) { - h.gatewayAddr = c.GatewaySvc - if c.SMTPCredentials != nil { - h.smtpCredentials = smtpclient.NewSMTPCredentials(c.SMTPCredentials) - } - h.meshDirectoryURL = c.MeshDirectoryURL -} - -func (h *invitesHandler) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - var head string - head, r.URL.Path = router.ShiftPath(r.URL.Path) - log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing") - - switch head { - case "": - h.generateInviteToken(w, r) - case "forward": - h.forwardInvite(w, r) - case "accept": - h.acceptInvite(w, r) - case "find-accepted-users": - h.findAcceptedUsers(w, r) - case "generate": - h.generate(w, r) - default: - w.WriteHeader(http.StatusNotFound) - } - }) -} +func (h *invitesHandler) init(c *config) error { + var err error -func (h *invitesHandler) generateInviteToken(w http.ResponseWriter, r *http.Request) { - - ctx := r.Context() - - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err) - return - } - - token, err := gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{}) + gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc) if err != nil { - WriteError(w, r, APIErrorServerError, "error generating token", err) - return - } - - if r.FormValue("recipient") != "" && h.smtpCredentials != nil { - - usr := ctxpkg.ContextMustGetUser(ctx) - - // TODO: the message body needs to point to the meshdirectory service - subject := fmt.Sprintf("ScienceMesh: %s wants to collaborate with you", usr.DisplayName) - body := "Hi,\n\n" + - usr.DisplayName + " (" + usr.Mail + ") wants to start sharing OCM resources with you. " + - "To accept the invite, please visit the following URL:\n" + - h.meshDirectoryURL + "?token=" + token.InviteToken.Token + "&providerDomain=" + usr.Id.Idp + "\n\n" + - "Alternatively, you can visit your mesh provider and use the following details:\n" + - "Token: " + token.InviteToken.Token + "\n" + - "ProviderDomain: " + usr.Id.Idp + "\n\n" + - "Best,\nThe ScienceMesh team" - - err = h.smtpCredentials.SendMail(r.FormValue("recipient"), subject, body) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending token by mail", err) - return - } - } - - jsonResponse, err := json.Marshal(token.InviteToken) - if err != nil { - WriteError(w, r, APIErrorServerError, "error marshalling token data", err) - return - } - - // Write response - _, err = w.Write(jsonResponse) - if err != nil { - WriteError(w, r, APIErrorServerError, "error writing token data", err) - return + return err } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) + h.gatewaySelector = gatewaySelector + return nil } -func (h *invitesHandler) forwardInvite(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - log := appctx.GetLogger(ctx) - contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - var token, providerDomain string - if err == nil && contentType == "application/json" { - defer r.Body.Close() - reqBody, err := io.ReadAll(r.Body) - if err == nil { - reqMap := make(map[string]string) - err = json.Unmarshal(reqBody, &reqMap) - if err == nil { - token, providerDomain = reqMap["token"], reqMap["providerDomain"] - } - } - } else { - token, providerDomain = r.FormValue("token"), r.FormValue("providerDomain") - } - if token == "" || providerDomain == "" { - WriteError(w, r, APIErrorInvalidParameter, "token and providerDomain must not be null", nil) - return - } - - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err) - return - } - - inviteToken := &invitepb.InviteToken{ - Token: token, - } - - providerInfo, err := gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ - Domain: providerDomain, - }) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc get invite by domain info request", err) - return - } - if providerInfo.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(providerInfo.Status.Message)) - return - } - - forwardInviteReq := &invitepb.ForwardInviteRequest{ - InviteToken: inviteToken, - OriginSystemProvider: providerInfo.ProviderInfo, - } - forwardInviteResponse, err := gatewayClient.ForwardInvite(ctx, forwardInviteReq) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc forward invite request", err) - return - } - if forwardInviteResponse.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(forwardInviteResponse.Status.Message)) - return - } - - _, err = w.Write([]byte("Accepted invite from: " + providerDomain)) - if err != nil { - WriteError(w, r, APIErrorServerError, "error writing token data", err) - return - } - w.WriteHeader(http.StatusOK) - - log.Info().Msgf("Invite forwarded to: %s", providerDomain) +type acceptInviteRequest struct { + Token string `json:"token"` + UserID string `json:"userID"` + RecipientProvider string `json:"recipientProvider"` + Name string `json:"name"` + Email string `json:"email"` } -func (h *invitesHandler) acceptInvite(w http.ResponseWriter, r *http.Request) { +// AcceptInvite informs avout an accepted invitation so that the users +// can initiate the OCM share creation. +func (h *invitesHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := appctx.GetLogger(ctx) - contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - var token, userID, recipientProvider, name, email string - if err == nil && contentType == "application/json" { - defer r.Body.Close() - reqBody, err := io.ReadAll(r.Body) - if err == nil { - reqMap := make(map[string]string) - err = json.Unmarshal(reqBody, &reqMap) - if err == nil { - token, userID, recipientProvider = reqMap["token"], reqMap["userID"], reqMap["recipientProvider"] - name, email = reqMap["name"], reqMap["email"] - } - } - } else { - token, userID, recipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider") - name, email = r.FormValue("name"), r.FormValue("email") - } - if token == "" || userID == "" || recipientProvider == "" { - WriteError(w, r, APIErrorInvalidParameter, "missing parameters in request", nil) - return - } - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) + req, err := getAcceptInviteRequest(r) if err != nil { - WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err) return } - clientIP, err := utils.GetClientIP(r) - if err != nil { - WriteError(w, r, APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err) + if req.Token == "" || req.UserID == "" || req.RecipientProvider == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token, userID and recipiendProvider must not be null", nil) return } - recipientProviderURL, err := url.Parse(recipientProvider) + clientIP, err := utils.GetClientIP(r) if err != nil { - WriteError(w, r, APIErrorServerError, fmt.Sprintf("error parseing recipientProvider URL: %s", recipientProvider), err) + reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err) return } providerInfo := ocmprovider.ProviderInfo{ - Domain: recipientProviderURL.Hostname(), + Domain: req.RecipientProvider, Services: []*ocmprovider.Service{ { Host: clientIP, }, }, } - + gatewayClient, err := h.gatewaySelector.Next() + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err) + return + } providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{ Provider: &providerInfo, }) if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc is provider allowed request", err) + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err) return } if providerAllowedResp.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message)) + reqres.WriteError(w, r, reqres.APIErrorUntrustedService, "provider not trusted", errors.New(providerAllowedResp.Status.Message)) return } userObj := &userpb.User{ Id: &userpb.UserId{ - OpaqueId: userID, - Idp: recipientProvider, - Type: userpb.UserType_USER_TYPE_PRIMARY, + OpaqueId: req.UserID, + Idp: req.RecipientProvider, + Type: userpb.UserType_USER_TYPE_FEDERATED, }, - Mail: email, - DisplayName: name, + Mail: req.Email, + DisplayName: req.Name, } acceptInviteRequest := &invitepb.AcceptInviteRequest{ InviteToken: &invitepb.InviteToken{ - Token: token, + Token: req.Token, }, RemoteUser: userObj, } acceptInviteResponse, err := gatewayClient.AcceptInvite(ctx, acceptInviteRequest) if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc accept invite request", err) + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc accept invite request", err) return } if acceptInviteResponse.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorServerError, "grpc accept invite request failed", errors.New(acceptInviteResponse.Status.Message)) - return - } - - log.Info().Msgf("User: %+v added to accepted users.", userObj) -} - -func (h *invitesHandler) findAcceptedUsers(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - - ctx := r.Context() - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err) - return + switch acceptInviteResponse.Status.Code { + case rpc.Code_CODE_NOT_FOUND: + reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil) + return + case rpc.Code_CODE_INVALID_ARGUMENT: + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil) + return + case rpc.Code_CODE_ALREADY_EXISTS: + reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil) + return + default: + reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+acceptInviteResponse.Status.Message, errors.New(acceptInviteResponse.Status.Message)) + return + } } - response, err := gatewayClient.FindAcceptedUsers(ctx, &invitepb.FindAcceptedUsersRequest{ - Filter: "", - }) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc find accepted users request", err) + if err := json.NewEncoder(w).Encode(&user{ + UserID: acceptInviteResponse.UserId.OpaqueId, + Email: acceptInviteResponse.Email, + Name: acceptInviteResponse.DisplayName, + }); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response", err) return } - - indentedResponse, _ := json.MarshalIndent(response, "", " ") - w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) - if _, err := w.Write(indentedResponse); err != nil { - log.Err(err).Msg("Error writing to ResponseWriter") - } -} - -func (h *invitesHandler) generate(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) + w.Header().Set("Content-Type", "application/json") - ctx := r.Context() - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) - if err != nil { - WriteError(w, r, APIErrorServerError, "error getting gateway grpc client", err) - return - } + log.Info().Str("user", fmt.Sprintf("%s@%s", userObj.Id.OpaqueId, userObj.Id.Idp)).Str("token", req.Token).Msg("added to accepted users") +} - response, err := gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{}) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc generate invite token request", err) - return - } +type user struct { + UserID string `json:"userID"` + Email string `json:"email"` + Name string `json:"name"` +} - indentedResponse, _ := json.MarshalIndent(response, "", " ") - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if _, err := w.Write(indentedResponse); err != nil { - log.Err(err).Msg("Error writing to ResponseWriter") +func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) { + var req acceptInviteRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + req.Token, req.UserID, req.RecipientProvider = r.FormValue("token"), r.FormValue("userID"), r.FormValue("recipientProvider") + req.Name, req.Email = r.FormValue("name"), r.FormValue("email") } + return &req, nil } diff --git a/internal/http/services/ocmd/notifications.go b/internal/http/services/ocmd/notifications.go index 48f5f9c043..5801f3e8fc 100644 --- a/internal/http/services/ocmd/notifications.go +++ b/internal/http/services/ocmd/notifications.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,27 +19,48 @@ package ocmd import ( + "io" + "mime" "net/http" + "github.com/cs3org/reva/v2/internal/http/services/reqres" "github.com/cs3org/reva/v2/pkg/appctx" - "github.com/cs3org/reva/v2/pkg/rhttp/router" ) -type notificationsHandler struct { +// var validate = validator.New() + +type notifHandler struct { } -func (h *notificationsHandler) init(c *Config) { +func (h *notifHandler) init(c *config) error { + return nil } -func (h *notificationsHandler) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) +// Notifications dispatches any notifications received from remote OCM sites +// according to the specifications at: +// https://cs3org.github.io/OCM-API/docs.html?branch=v1.1.0&repo=OCM-API&user=cs3org#/paths/~1notifications/post +func (h *notifHandler) Notifications(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + req, err := getNotification(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) + return + } - var head string - head, r.URL.Path = router.ShiftPath(r.URL.Path) + // TODO(lopresti) this is all to be implemented. For now we just log what we got + log.Debug().Msgf("Received OCM notification: %+v", req) - log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing") + // this is to please Nextcloud + w.WriteHeader(http.StatusCreated) +} - w.WriteHeader(http.StatusOK) - }) +func getNotification(r *http.Request) (string, error) { + // var req notificationRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + bytes, _ := io.ReadAll(r.Body) + return string(bytes), nil + } + return "", nil } diff --git a/internal/http/services/ocmd/ocm.go b/internal/http/services/ocmd/ocm.go new file mode 100644 index 0000000000..caa93833ba --- /dev/null +++ b/internal/http/services/ocmd/ocm.go @@ -0,0 +1,118 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmd + +import ( + "net/http" + + "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/rhttp/global" + "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/utils/cfg" + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog" +) + +func init() { + global.Register("ocmd", New) +} + +type config struct { + Prefix string `mapstructure:"prefix"` + GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` + ExposeRecipientDisplayName bool `mapstructure:"expose_recipient_display_name"` +} + +func (c *config) ApplyDefaults() { + c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) + if c.Prefix == "" { + c.Prefix = "ocm" + } +} + +type svc struct { + Conf *config + router chi.Router +} + +// New returns a new ocmd object, that implements +// the OCM APIs specified in https://cs3org.github.io/OCM-API/docs.html +func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { + var c config + if err := cfg.Decode(m, &c); err != nil { + return nil, err + } + + r := chi.NewRouter() + s := &svc{ + Conf: &c, + router: r, + } + + if err := s.routerInit(); err != nil { + return nil, err + } + + return s, nil +} + +func (s *svc) routerInit() error { + sharesHandler := new(sharesHandler) + invitesHandler := new(invitesHandler) + notifHandler := new(notifHandler) + + if err := sharesHandler.init(s.Conf); err != nil { + return err + } + if err := invitesHandler.init(s.Conf); err != nil { + return err + } + if err := notifHandler.init(s.Conf); err != nil { + return err + } + + s.router.Post("/shares", sharesHandler.CreateShare) + s.router.Post("/invite-accepted", invitesHandler.AcceptInvite) + s.router.Post("/notifications", notifHandler.Notifications) + return nil +} + +// Close performs cleanup. +func (s *svc) Close() error { + return nil +} + +func (s *svc) Prefix() string { + return s.Conf.Prefix +} + +func (s *svc) Unprotected() []string { + return []string{"/invite-accepted", "/shares", "/notifications"} +} + +func (s *svc) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + log.Debug().Str("path", r.URL.Path).Msg("ocm routing") + + // unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments + r.URL.RawPath = "" + s.router.ServeHTTP(w, r) + }) +} diff --git a/internal/http/services/ocmd/ocmd.go b/internal/http/services/ocmd/ocmd.go deleted file mode 100644 index 9448318521..0000000000 --- a/internal/http/services/ocmd/ocmd.go +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2018-2021 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package ocmd - -import ( - "net/http" - - "github.com/cs3org/reva/v2/pkg/appctx" - "github.com/cs3org/reva/v2/pkg/rhttp/global" - "github.com/cs3org/reva/v2/pkg/rhttp/router" - "github.com/cs3org/reva/v2/pkg/sharedconf" - "github.com/cs3org/reva/v2/pkg/smtpclient" - "github.com/mitchellh/mapstructure" - "github.com/rs/zerolog" -) - -func init() { - global.Register("ocmd", New) -} - -// Config holds the config options that need to be passed down to all ocdav handlers -type Config struct { - SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials"` - Prefix string `mapstructure:"prefix"` - Host string `mapstructure:"host"` - GatewaySvc string `mapstructure:"gatewaysvc"` - MeshDirectoryURL string `mapstructure:"mesh_directory_url"` - Config configData `mapstructure:"config"` -} - -func (c *Config) init() { - c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) - - // if c.Prefix == "" { - // c.Prefix = "ocm" - // } -} - -type svc struct { - Conf *Config - SharesHandler *sharesHandler - NotificationsHandler *notificationsHandler - ConfigHandler *configHandler - InvitesHandler *invitesHandler - SendHandler *sendHandler -} - -// New returns a new ocmd object -func New(m map[string]interface{}, log *zerolog.Logger) (global.Service, error) { - - conf := &Config{} - if err := mapstructure.Decode(m, conf); err != nil { - return nil, err - } - conf.init() - - s := &svc{ - Conf: conf, - } - s.SharesHandler = new(sharesHandler) - s.NotificationsHandler = new(notificationsHandler) - s.ConfigHandler = new(configHandler) - s.InvitesHandler = new(invitesHandler) - s.SendHandler = new(sendHandler) - s.SharesHandler.init(s.Conf) - s.NotificationsHandler.init(s.Conf) - log.Debug().Str("initializing ConfigHandler Host", s.Conf.Host) - - s.ConfigHandler.init(s.Conf) - s.InvitesHandler.init(s.Conf) - s.SendHandler.init(s.Conf) - - return s, nil -} - -// Close performs cleanup. -func (s *svc) Close() error { - return nil -} - -func (s *svc) Prefix() string { - return s.Conf.Prefix -} - -func (s *svc) Unprotected() []string { - return []string{"/invites/accept", "/shares", "/ocm-provider", "/notifications"} -} - -func (s *svc) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - ctx := r.Context() - log := appctx.GetLogger(ctx) - - var head string - head, r.URL.Path = router.ShiftPath(r.URL.Path) - log.Debug().Str("head", head).Str("tail", r.URL.Path).Msg("http routing") - - switch head { - case "ocm-provider": - s.ConfigHandler.Handler().ServeHTTP(w, r) - return - case "shares": - s.SharesHandler.Handler().ServeHTTP(w, r) - return - case "notifications": - s.NotificationsHandler.Handler().ServeHTTP(w, r) - return - case "invites": - s.InvitesHandler.Handler().ServeHTTP(w, r) - return - case "send": - s.SendHandler.Handler().ServeHTTP(w, r) - } - - log.Warn().Msg("request not handled") - w.WriteHeader(http.StatusNotFound) - }) -} diff --git a/internal/http/services/ocmd/protocols.go b/internal/http/services/ocmd/protocols.go new file mode 100644 index 0000000000..0c77358387 --- /dev/null +++ b/internal/http/services/ocmd/protocols.go @@ -0,0 +1,160 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmd + +import ( + "encoding/json" + "errors" + "fmt" + "reflect" + "strings" + + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + ocmshare "github.com/cs3org/reva/v2/pkg/ocm/share" + utils "github.com/cs3org/reva/v2/pkg/utils" +) + +// Protocols is the list of protocols. +type Protocols []Protocol + +// Protocol represents the way of access the resource +// in the OCM share. +type Protocol interface { + // ToOCMProtocol convert the protocol to a ocm Protocol struct + ToOCMProtocol() *ocm.Protocol +} + +// protocols supported by the OCM API + +// WebDAV contains the parameters for the WebDAV protocol. +type WebDAV struct { + SharedSecret string `json:"sharedSecret" validate:"required"` + Permissions []string `json:"permissions" validate:"required,dive,required,oneof=read write share"` + URL string `json:"url" validate:"required"` +} + +// ToOCMProtocol convert the protocol to a ocm Protocol struct. +func (w *WebDAV) ToOCMProtocol() *ocm.Protocol { + perms := &ocm.SharePermissions{ + Permissions: &providerv1beta1.ResourcePermissions{}, + } + for _, p := range w.Permissions { + switch p { + case "read": + perms.Permissions.GetPath = true + perms.Permissions.InitiateFileDownload = true + perms.Permissions.ListContainer = true + perms.Permissions.Stat = true + case "write": + perms.Permissions.InitiateFileUpload = true + case "share": + perms.Reshare = true + } + } + + return ocmshare.NewWebDAVProtocol(w.URL, w.SharedSecret, perms) +} + +// Webapp contains the parameters for the Webapp protocol. +type Webapp struct { + URITemplate string `json:"uriTemplate" validate:"required"` + ViewMode string `json:"viewMode" validate:"required,dive,required,oneof=view read write"` +} + +// ToOCMProtocol convert the protocol to a ocm Protocol struct. +func (w *Webapp) ToOCMProtocol() *ocm.Protocol { + return ocmshare.NewWebappProtocol(w.URITemplate, utils.GetAppViewMode(w.ViewMode)) +} + +// Datatx contains the parameters for the Datatx protocol. +type Datatx struct { + SharedSecret string `json:"sharedSecret" validate:"required"` + SourceURI string `json:"srcUri" validate:"required"` + Size uint64 `json:"size" validate:"required"` +} + +// ToOCMProtocol convert the protocol to a ocm Protocol struct. +func (w *Datatx) ToOCMProtocol() *ocm.Protocol { + return ocmshare.NewTransferProtocol(w.SourceURI, w.SharedSecret, w.Size) +} + +var protocolImpl = map[string]reflect.Type{ + "webdav": reflect.TypeOf(WebDAV{}), + "webapp": reflect.TypeOf(Webapp{}), + "datatx": reflect.TypeOf(Datatx{}), +} + +// UnmarshalJSON implements the Unmarshaler interface. +func (p *Protocols) UnmarshalJSON(data []byte) error { + var prot map[string]json.RawMessage + if err := json.Unmarshal(data, &prot); err != nil { + return err + } + + *p = []Protocol{} + + for name, d := range prot { + var res Protocol + + // we do not support the OCM v1.0 properties for now, therefore just skip or bail out + if name == "name" { + continue + } + if name == "options" { + var opt map[string]any + if err := json.Unmarshal(d, &opt); err != nil || len(opt) > 0 { + return fmt.Errorf("protocol options not supported: %s", string(d)) + } + continue + } + ctype, ok := protocolImpl[name] + if !ok { + return fmt.Errorf("protocol %s not recognised", name) + } + res = reflect.New(ctype).Interface().(Protocol) + if err := json.Unmarshal(d, &res); err != nil { + return err + } + + *p = append(*p, res) + } + return nil +} + +// MarshalJSON implements the Marshaler interface. +func (p Protocols) MarshalJSON() ([]byte, error) { + if len(p) == 0 { + return nil, errors.New("no protocol defined") + } + d := make(map[string]any) + for _, prot := range p { + d[getProtocolName(prot)] = prot + } + // fill in the OCM v1.0 properties + d["name"] = "multi" + d["options"] = map[string]any{} + return json.Marshal(d) +} + +func getProtocolName(p Protocol) string { + n := reflect.TypeOf(p).String() + s := strings.Split(n, ".") + return strings.ToLower(s[len(s)-1]) +} diff --git a/internal/http/services/ocmd/protocols_test.go b/internal/http/services/ocmd/protocols_test.go new file mode 100644 index 0000000000..35d857f062 --- /dev/null +++ b/internal/http/services/ocmd/protocols_test.go @@ -0,0 +1,249 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package ocmd + +import ( + "encoding/json" + "reflect" + "testing" + + "github.com/gdexlab/go-render/render" +) + +func TestUnmarshalProtocol(t *testing.T) { + tests := []struct { + raw string + expected Protocols + err string + }{ + { + raw: "{}", + expected: []Protocol{}, + }, + { + raw: `{"name":"foo","options":{ }}`, + expected: []Protocol{}, + }, + { + raw: `{"name":"foo","options":{"unsupported":"value"}}`, + err: `protocol options not supported: {"unsupported":"value"}`, + }, + { + raw: `{"unsupported":{}}`, + err: "protocol unsupported not recognised", + }, + { + raw: `{"name":"multi","options":{},"webdav":{"sharedSecret":"secret","permissions":["read","write"],"url":"http://example.org"}}`, + expected: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read", "write"}, + URL: "http://example.org", + }, + }, + }, + { + raw: `{"name":"multi","options":{},"webapp":{"uriTemplate":"http://example.org/{test}"}}`, + expected: []Protocol{ + &Webapp{ + URITemplate: "http://example.org/{test}", + }, + }, + }, + { + raw: `{"name":"multi","options":{},"datatx":{"sharedSecret":"secret","srcUri":"http://example.org","size":10}}`, + expected: []Protocol{ + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org", + Size: 10, + }, + }, + }, + { + raw: `{"name":"multi","options":{},"webdav":{"sharedSecret":"secret","permissions":["read","write"],"url":"http://example.org"},"webapp":{"uriTemplate":"http://example.org/{test}"},"datatx":{"sharedSecret":"secret","srcUri":"http://example.org","size":10}}`, + expected: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read", "write"}, + URL: "http://example.org", + }, + &Webapp{ + URITemplate: "http://example.org/{test}", + }, + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org", + Size: 10, + }, + }, + }, + } + + for _, tt := range tests { + var got Protocols + err := json.Unmarshal([]byte(tt.raw), &got) + if err != nil && err.Error() != tt.err { + t.Fatalf("not expected error. got=%+v expected=%+v", err, tt.err) + } + + if tt.err == "" { + if !protocolsEqual(got, tt.expected) { + t.Fatalf("result does not match with expected. got=%+v expected=%+v", render.AsCode(got), render.AsCode(tt.expected)) + } + } + } +} + +func protocolsToMap(p Protocols) map[string]Protocol { + m := make(map[string]Protocol) + for _, prot := range p { + switch prot.(type) { + case *WebDAV: + m["webdav"] = prot + case *Webapp: + m["webapp"] = prot + case *Datatx: + m["datatx"] = prot + } + } + return m +} + +func protocolsEqual(p1, p2 Protocols) bool { + return reflect.DeepEqual(protocolsToMap(p1), protocolsToMap(p2)) +} + +func TestMarshalProtocol(t *testing.T) { + tests := []struct { + in Protocols + expected map[string]any + err string + }{ + { + in: []Protocol{}, + err: "json: error calling MarshalJSON for type ocmd.Protocols: no protocol defined", + }, + { + in: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read"}, + URL: "http://example.org", + }, + }, + expected: map[string]any{ + "name": "multi", + "options": map[string]any{}, + "webdav": map[string]any{ + "sharedSecret": "secret", + "permissions": []any{"read"}, + "url": "http://example.org", + }, + }, + }, + { + in: []Protocol{ + &Webapp{ + URITemplate: "http://example.org", + ViewMode: "read", + }, + }, + expected: map[string]any{ + "name": "multi", + "options": map[string]any{}, + "webapp": map[string]any{ + "uriTemplate": "http://example.org", + "viewMode": "read", + }, + }, + }, + { + in: []Protocol{ + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org/source", + Size: 10, + }, + }, + expected: map[string]any{ + "name": "multi", + "options": map[string]any{}, + "datatx": map[string]any{ + "sharedSecret": "secret", + "srcUri": "http://example.org/source", + "size": float64(10), + }, + }, + }, + { + in: []Protocol{ + &WebDAV{ + SharedSecret: "secret", + Permissions: []string{"read"}, + URL: "http://example.org", + }, + &Webapp{ + URITemplate: "http://example.org", + ViewMode: "read", + }, + &Datatx{ + SharedSecret: "secret", + SourceURI: "http://example.org/source", + Size: 10, + }, + }, + expected: map[string]any{ + "name": "multi", + "options": map[string]any{}, + "webdav": map[string]any{ + "sharedSecret": "secret", + "permissions": []any{"read"}, + "url": "http://example.org", + }, + "webapp": map[string]any{ + "uriTemplate": "http://example.org", + "viewMode": "read", + }, + "datatx": map[string]any{ + "sharedSecret": "secret", + "srcUri": "http://example.org/source", + "size": float64(10), + }, + }, + }, + } + + for _, tt := range tests { + d, err := json.Marshal(tt.in) + if err != nil && err.Error() != tt.err { + t.Fatalf("not expected error. got=%+v expected=%+v", err, tt.err) + } + if err == nil { + var got map[string]any + if err := json.Unmarshal(d, &got); err != nil { + t.Fatalf("not expected error %+v with input %+v", err, tt.in) + } + + if !reflect.DeepEqual(tt.expected, got) { + t.Fatalf("result does not match with expected. got=%+v expected=%+v", render.AsCode(got), render.AsCode(tt.expected)) + } + } + } +} diff --git a/internal/http/services/ocmd/send.go b/internal/http/services/ocmd/send.go deleted file mode 100644 index a4a84e284e..0000000000 --- a/internal/http/services/ocmd/send.go +++ /dev/null @@ -1,200 +0,0 @@ -// Copyright 2018-2021 CERN -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// In applying this license, CERN does not waive the privileges and immunities -// granted to it by virtue of its status as an Intergovernmental Organization -// or submit itself to any jurisdiction. - -package ocmd - -import ( - "context" - "encoding/json" - "errors" - "io" - "net/http" - "strconv" - - gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" - userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" - 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" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/v2/pkg/appctx" - ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" - "google.golang.org/grpc/metadata" - - "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" -) - -type sendHandler struct { - GatewaySvc string -} - -func (h *sendHandler) init(c *Config) { - h.GatewaySvc = c.GatewaySvc -} - -func (h *sendHandler) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log := appctx.GetLogger(r.Context()) - defer r.Body.Close() - reqBody, err := io.ReadAll(r.Body) - if err != nil { - log.Error().Msg("cannot read POST body!") - w.WriteHeader(http.StatusInternalServerError) - return - } - - reqMap := make(map[string]string) - err = json.Unmarshal(reqBody, &reqMap) - if err != nil { - log.Error().Msg("cannot parse POST body!") - w.WriteHeader(http.StatusInternalServerError) - return - } - sourcePath, targetPath, sharedSecret := reqMap["sourcePath"], reqMap["targetPath"], reqMap["sharedSecret"] - recipientUsername, recipientHost := reqMap["recipientUsername"], reqMap["recipientHost"] - loginType, loginUsername, loginPassword := reqMap["loginType"], reqMap["loginUsername"], reqMap["loginPassword"] - - // "sourcePath": "other", - // "targetPath": "sciencemesh\/other", - // "type": "dir", (unused) - // "recipientUsername": "marie", - // "recipientHost": "revanc2.docker", - // "loginType": "basic", - // "loginUsername": "einstein", - // "loginPassword": "Ny4Nv6WLoC1o70kVgrVOZLZ2vRgPjuej" - - gatewayAddr := h.GatewaySvc - gatewayClient, err := pool.GetGatewayServiceClient(gatewayAddr) - if err != nil { - log.Error().Msg("cannot get grpc client!") - w.WriteHeader(http.StatusInternalServerError) - return - } - loginReq := &gateway.AuthenticateRequest{ - Type: loginType, - ClientId: loginUsername, - ClientSecret: loginPassword, - } - - loginCtx := context.Background() - res, err := gatewayClient.Authenticate(loginCtx, loginReq) - if err != nil { - log.Error().Msg("error logging in") - w.WriteHeader(http.StatusInternalServerError) - return - } - authCtx := context.Background() - - authCtx = ctxpkg.ContextSetToken(authCtx, res.Token) - authCtx = metadata.AppendToOutgoingContext(authCtx, ctxpkg.TokenHeader, res.Token) - - // copied from cmd/reva/public-share-create.go: - ref := &provider.Reference{Path: sourcePath} - - req := &provider.StatRequest{Ref: ref} - res2, err := gatewayClient.Stat(authCtx, req) - if err != nil { - log.Error().Msg("error sending: stat file/folder to share") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if res2.Status.Code != rpc.Code_CODE_OK { - log.Error().Msg("error returned: stat file/folder to share") - w.WriteHeader(http.StatusInternalServerError) - return - } - - // see cmd/reva/share-creat.go:getSharePerm - readerPermission := &provider.ResourcePermissions{ - GetPath: true, - InitiateFileDownload: true, - ListFileVersions: true, - ListContainer: true, - Stat: true, - } - - grant := &ocm.ShareGrant{ - Permissions: &ocm.SharePermissions{ - Permissions: readerPermission, - }, - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - Id: &provider.Grantee_UserId{ - UserId: &userpb.UserId{ - Idp: recipientHost, - OpaqueId: recipientUsername, - }, - }, - }, - } - recipientProviderInfo, err := gatewayClient.GetInfoByDomain(authCtx, &ocmprovider.GetInfoByDomainRequest{ - Domain: recipientHost, - }) - if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc get invite by domain info request", err) - return - } - if recipientProviderInfo.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorServerError, "grpc forward invite request failed", errors.New(recipientProviderInfo.Status.Message)) - return - } - - shareRequest := &ocm.CreateOCMShareRequest{ - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "plain", - Value: []byte(strconv.Itoa(0)), - }, - "name": { - Decoder: "plain", - Value: []byte(targetPath), - }, - "protocol": { - Decoder: "plain", - Value: []byte("webdav"), // TODO: support datatx too - }, - "token": { - Decoder: "plain", - Value: []byte(sharedSecret), - }, - }, - }, - ResourceId: res2.Info.Id, - Grant: grant, - RecipientMeshProvider: recipientProviderInfo.ProviderInfo, - } - - shareRes, err := gatewayClient.CreateOCMShare(authCtx, shareRequest) - if err != nil { - log.Error().Msg("error sending: CreateShare") - w.WriteHeader(http.StatusInternalServerError) - return - } - - if shareRes.Status.Code != rpc.Code_CODE_OK { - log.Error().Msg("error returned: CreateShare") - w.WriteHeader(http.StatusInternalServerError) - return - } - - w.WriteHeader(http.StatusOK) - }) -} diff --git a/internal/http/services/ocmd/shares.go b/internal/http/services/ocmd/shares.go index ac9700daf3..1d3c455a56 100644 --- a/internal/http/services/ocmd/shares.go +++ b/internal/http/services/ocmd/shares.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,102 +22,83 @@ import ( "encoding/json" "errors" "fmt" - "io" - "math" "mime" "net/http" - "reflect" "strings" - "time" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" ocmcore "github.com/cs3org/go-cs3apis/cs3/ocm/core/v1beta1" ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/v2/internal/http/services/reqres" "github.com/cs3org/reva/v2/pkg/appctx" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" "github.com/cs3org/reva/v2/pkg/utils" + "github.com/go-playground/validator/v10" ) +var validate = validator.New() + type sharesHandler struct { - gatewayAddr string + gatewaySelector *pool.Selector[gateway.GatewayAPIClient] + exposeRecipientDisplayName bool } -func (h *sharesHandler) init(c *Config) { - h.gatewayAddr = c.GatewaySvc -} +func (h *sharesHandler) init(c *config) error { + var err error -func (h *sharesHandler) Handler() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gatewaySelector, err := pool.GatewaySelector(c.GatewaySvc) + if err != nil { + return err + } + h.gatewaySelector = gatewaySelector - switch r.Method { - case http.MethodPost: - h.createShare(w, r) - default: - WriteError(w, r, APIErrorInvalidParameter, "Only POST method is allowed", nil) - } - }) + h.exposeRecipientDisplayName = c.ExposeRecipientDisplayName + return nil } -func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { +type createShareRequest struct { + ShareWith string `json:"shareWith" validate:"required"` // identifier of the recipient of the share + Name string `json:"name" validate:"required"` // name of the resource + Description string `json:"description"` // (optional) description of the resource + ProviderID string `json:"providerId" validate:"required"` // unique identifier of the resource at provider side + Owner string `json:"owner" validate:"required"` // unique identifier of the owner at provider side + Sender string `json:"sender" validate:"required"` // unique indentifier of the user who wants to share the resource at provider side + OwnerDisplayName string `json:"ownerDisplayName"` // display name of the owner of the resource + SenderDisplayName string `json:"senderDisplayName"` // dispay name of the user who wants to share the resource + ShareType string `json:"shareType" validate:"required,oneof=user group"` // recipient share type (user or group) + ResourceType string `json:"resourceType" validate:"required,oneof=file folder"` + Expiration uint64 `json:"expiration"` + Protocols Protocols `json:"protocol" validate:"required"` +} + +// CreateShare sends all the informations to the consumer needed to start +// synchronization between the two services. +func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { ctx := r.Context() log := appctx.GetLogger(ctx) - contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) - var shareWith, meshProvider, resource, providerID, owner string - var protocol map[string]interface{} - if err == nil && contentType == "application/json" { - defer r.Body.Close() - reqBody, err := io.ReadAll(r.Body) - if err == nil { - reqMap := make(map[string]interface{}) - err = json.Unmarshal(reqBody, &reqMap) - if err == nil { - meshProvider = reqMap["meshProvider"].(string) // FIXME: get this from sharedBy string? - shareWith, protocol = reqMap["shareWith"].(string), reqMap["protocol"].(map[string]interface{}) - resource, owner = reqMap["name"].(string), reqMap["owner"].(string) - // Note that if an OCM request were to go directly from a Nextcloud server - // to a Reva server, it will (incorrectly) sends an integer provider_id instead a string one. - // This doesn't happen when using the sciencemesh-nextcloud app, but in order to make the OCM - // test suite pass, this code works around that: - if reflect.ValueOf(reqMap["providerId"]).Kind() == reflect.Float64 { - providerID = fmt.Sprintf("%d", int(math.Round(reqMap["providerId"].(float64)))) - } else { - providerID = reqMap["providerId"].(string) - } - } else { - WriteError(w, r, APIErrorInvalidParameter, "could not parse json request body", nil) - } - } - } else { - var protocolJSON string - shareWith, protocolJSON, meshProvider = r.FormValue("shareWith"), r.FormValue("protocol"), r.FormValue("meshProvider") - resource, providerID, owner = r.FormValue("name"), r.FormValue("providerId"), r.FormValue("owner") - err = json.Unmarshal([]byte(protocolJSON), &protocol) - if err != nil { - WriteError(w, r, APIErrorInvalidParameter, "invalid protocol parameters", nil) - } - } - - if resource == "" || providerID == "" || owner == "" { - WriteError(w, r, APIErrorInvalidParameter, "missing details about resource to be shared", nil) - return - } - if shareWith == "" || protocol["name"] == "" || meshProvider == "" { - WriteError(w, r, APIErrorInvalidParameter, "missing request parameters", nil) + req, err := getCreateShareRequest(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) + _, meshProvider, err := getIDAndMeshProvider(req.Sender) + log.Debug().Msgf("Determined Mesh Provider '%s' from req.Sender '%s'", meshProvider, req.Sender) if err != nil { - WriteError(w, r, APIErrorServerError, "error getting storage grpc client", err) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } clientIP, err := utils.GetClientIP(r) if err != nil { - WriteError(w, r, APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err) + reqres.WriteError(w, r, reqres.APIErrorServerError, fmt.Sprintf("error retrieving client IP from request: %s", r.RemoteAddr), err) return } providerInfo := ocmprovider.ProviderInfo{ @@ -128,128 +109,154 @@ func (h *sharesHandler) createShare(w http.ResponseWriter, r *http.Request) { }, }, } - + gatewayClient, err := h.gatewaySelector.Next() + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error getting gateway client", err) + return + } providerAllowedResp, err := gatewayClient.IsProviderAllowed(ctx, &ocmprovider.IsProviderAllowedRequest{ Provider: &providerInfo, }) if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc is provider allowed request", err) + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc is provider allowed request", err) return } if providerAllowedResp.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message)) + reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "provider not authorized", errors.New(providerAllowedResp.Status.Message)) return } - shareWithParts := strings.Split(shareWith, "@") - userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ - UserId: &userpb.UserId{OpaqueId: shareWithParts[0]}, SkipFetchingUserGroups: true, - }) + shareWith, _, err := getIDAndMeshProvider(req.ShareWith) if err != nil { - WriteError(w, r, APIErrorServerError, "error searching recipient", err) - return - } - if userRes.Status.Code != rpc.Code_CODE_OK { - WriteError(w, r, APIErrorNotFound, "user not found", errors.New(userRes.Status.Message)) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - var permissions conversions.Permissions - var token string - options, ok := protocol["options"].(map[string]interface{}) - if !ok { - WriteError(w, r, APIErrorInvalidParameter, "protocol: webdav token not provided", nil) + userRes, err := gatewayClient.GetUser(ctx, &userpb.GetUserRequest{ + UserId: &userpb.UserId{OpaqueId: shareWith}, SkipFetchingUserGroups: true, + }) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error searching recipient", err) return } - - token, ok = options["sharedSecret"].(string) - if !ok { - token, ok = options["token"].(string) - if !ok { - WriteError(w, r, APIErrorInvalidParameter, "protocol: webdav token not provided", nil) - return - } - } - var role *conversions.Role - pval, ok := options["permissions"].(int) - if !ok { - WriteError(w, r, APIErrorInvalidParameter, "permissions not provided", nil) + if userRes.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorNotFound, "user not found", errors.New(userRes.Status.Message)) return } - permissions, err = conversions.NewPermissions(pval) + owner, err := getUserIDFromOCMUser(req.Owner) if err != nil { - WriteError(w, r, APIErrorInvalidParameter, err.Error(), nil) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - role = conversions.RoleFromOCSPermissions(permissions) - val, err := json.Marshal(role.CS3ResourcePermissions()) + sender, err := getUserIDFromOCMUser(req.Sender) if err != nil { - WriteError(w, r, APIErrorServerError, "could not encode role", nil) + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, err.Error(), nil) return } - ownerID := &userpb.UserId{ - OpaqueId: owner, - Idp: meshProvider, - Type: userpb.UserType_USER_TYPE_PRIMARY, - } createShareReq := &ocmcore.CreateOCMCoreShareRequest{ - Name: resource, - ProviderId: providerID, - Owner: ownerID, - ShareWith: userRes.User.GetId(), - Protocol: &ocmcore.Protocol{ - Name: protocol["name"].(string), - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "permissions": { - Decoder: "json", - Value: val, - }, - "token": { - Decoder: "plain", - Value: []byte(token), - }, - }, - }, - }, + Description: req.Description, + Name: req.Name, + ResourceId: req.ProviderID, + Owner: owner, + Sender: sender, + ShareWith: userRes.User.Id, + ResourceType: getResourceTypeFromOCMRequest(req.ResourceType), + ShareType: getOCMShareType(req.ShareType), + Protocols: getProtocols(req.Protocols), } - createShareResponse, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq) + + if req.Expiration != 0 { + createShareReq.Expiration = &types.Timestamp{ + Seconds: req.Expiration, + } + } + + createShareResp, err := gatewayClient.CreateOCMCoreShare(ctx, createShareReq) if err != nil { - WriteError(w, r, APIErrorServerError, "error sending a grpc create ocm core share request", err) + reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", err) return } - if createShareResponse.Status.Code != rpc.Code_CODE_OK { - if createShareResponse.Status.Code == rpc.Code_CODE_NOT_FOUND { - WriteError(w, r, APIErrorNotFound, "not found", nil) - return - } - WriteError(w, r, APIErrorServerError, "grpc create ocm core share request failed", errors.New(createShareResponse.Status.Message)) + + if userRes.Status.Code != rpc.Code_CODE_OK { + // TODO: define errors in the cs3apis + reqres.WriteError(w, r, reqres.APIErrorServerError, "error creating ocm share", errors.New(createShareResp.Status.Message)) return } - timeCreated := createShareResponse.Created - jsonOut, err := json.Marshal( - map[string]string{ - "id": createShareResponse.Id, - "createdAt": time.Unix(int64(timeCreated.Seconds), int64(timeCreated.Nanos)).String(), - }, - ) - if err != nil { - WriteError(w, r, APIErrorServerError, "error marshalling share data", err) - return + response := map[string]any{} + + if h.exposeRecipientDisplayName { + response["recipientDisplayName"] = userRes.User.DisplayName } + _ = json.NewEncoder(w).Encode(response) w.WriteHeader(http.StatusCreated) - w.Header().Set("Content-Type", "application/json") +} - _, err = w.Write(jsonOut) +func getUserIDFromOCMUser(user string) (*userpb.UserId, error) { + id, idp, err := getIDAndMeshProvider(user) if err != nil { - WriteError(w, r, APIErrorServerError, "error writing shares data", err) - return + return nil, err + } + return &userpb.UserId{ + OpaqueId: id, + Idp: idp, + // the remote user is a federated account for the local reva + Type: userpb.UserType_USER_TYPE_FEDERATED, + }, nil +} + +func getIDAndMeshProvider(user string) (string, string, error) { + // the user is in the form of dimitri@apiwise.nl + split := strings.Split(user, "@") + if len(split) < 2 { + return "", "", errors.New("not in the form @") + } + return strings.Join(split[:len(split)-1], "@"), split[len(split)-1], nil +} + +func getCreateShareRequest(r *http.Request) (*createShareRequest, error) { + var req createShareRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + return nil, errors.New("body request not recognised") } + // validate the request + if err := validate.Struct(req); err != nil { + return nil, err + } + return &req, nil +} + +func getResourceTypeFromOCMRequest(t string) providerpb.ResourceType { + switch t { + case "file": + return providerpb.ResourceType_RESOURCE_TYPE_FILE + case "folder": + return providerpb.ResourceType_RESOURCE_TYPE_CONTAINER + default: + return providerpb.ResourceType_RESOURCE_TYPE_INVALID + } +} - log.Info().Msg("Share created.") +func getOCMShareType(t string) ocm.ShareType { + if t == "user" { + return ocm.ShareType_SHARE_TYPE_USER + } + return ocm.ShareType_SHARE_TYPE_GROUP +} + +func getProtocols(p Protocols) []*ocm.Protocol { + prot := make([]*ocm.Protocol, 0, len(p)) + for _, data := range p { + prot = append(prot, data.ToOCMProtocol()) + } + return prot } diff --git a/internal/http/services/owncloud/ocdav/config/config.go b/internal/http/services/owncloud/ocdav/config/config.go index 2e00de97d2..77c0153d78 100644 --- a/internal/http/services/owncloud/ocdav/config/config.go +++ b/internal/http/services/owncloud/ocdav/config/config.go @@ -16,6 +16,7 @@ type Config struct { // /users///docs WebdavNamespace string `mapstructure:"webdav_namespace"` SharesNamespace string `mapstructure:"shares_namespace"` + OCMNamespace string `mapstructure:"ocm_namespace"` GatewaySvc string `mapstructure:"gatewaysvc"` Timeout int64 `mapstructure:"timeout"` Insecure bool `mapstructure:"insecure"` diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index 6a2d66b911..93e12da1de 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -22,6 +22,7 @@ import ( "context" "net/http" "path" + "path/filepath" "strings" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" @@ -56,6 +57,7 @@ type DavHandler struct { PublicFolderHandler *WebDavHandler PublicFileHandler *PublicFileHandler SharesHandler *WebDavHandler + OCMSharesHandler *WebDavHandler } func (h *DavHandler) init(c *config.Config) error { @@ -95,6 +97,11 @@ func (h *DavHandler) init(c *config.Config) error { return err } + h.OCMSharesHandler = new(WebDavHandler) + if err := h.OCMSharesHandler.init(c.OCMNamespace, true); err != nil { + return err + } + return nil } @@ -170,6 +177,60 @@ func (h *DavHandler) Handler(s *svc) http.Handler { ctx = context.WithValue(ctx, net.CtxKeyBaseURI, base) r = r.WithContext(ctx) h.MetaHandler.Handler(s).ServeHTTP(w, r) + case "ocm": + base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "ocm") + ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) + c, err := s.gatewaySelector.Next() + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + // OC10 and Nextcloud (OCM 1.0) are using basic auth for carrying the + // shared token. + var token string + username, _, ok := r.BasicAuth() + if ok { + // OCM 1.0 + token = username + r.URL.Path = filepath.Join("/", token, r.URL.Path) + ctx = context.WithValue(ctx, net.CtxOCM10, true) + } else { + token, _ = router.ShiftPath(r.URL.Path) + ctx = context.WithValue(ctx, net.CtxOCM10, false) + } + + authRes, err := handleOCMAuth(ctx, c, token) + switch { + case err != nil: + log.Error().Err(err).Msg("error during ocm authentication") + w.WriteHeader(http.StatusInternalServerError) + return + case authRes.Status.Code == rpc.Code_CODE_PERMISSION_DENIED: + log.Debug().Str("token", token).Msg("permission denied") + fallthrough + case authRes.Status.Code == rpc.Code_CODE_UNAUTHENTICATED: + log.Debug().Str("token", token).Msg("unauthorized") + w.WriteHeader(http.StatusUnauthorized) + return + case authRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + log.Debug().Str("token", token).Msg("not found") + w.WriteHeader(http.StatusNotFound) + return + case authRes.Status.Code != rpc.Code_CODE_OK: + log.Error().Str("token", token).Interface("status", authRes.Status).Msg("grpc auth request failed") + w.WriteHeader(http.StatusInternalServerError) + return + } + + ctx = ctxpkg.ContextSetToken(ctx, authRes.Token) + ctx = ctxpkg.ContextSetUser(ctx, authRes.User) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, authRes.Token) + + log.Debug().Str("token", token).Interface("user", authRes.User).Msg("OCM user authenticated") + + r = r.WithContext(ctx) + h.OCMSharesHandler.Handler(s).ServeHTTP(w, r) case "trash-bin": base := path.Join(ctx.Value(net.CtxKeyBaseURI).(string), "trash-bin") ctx := context.WithValue(ctx, net.CtxKeyBaseURI, base) @@ -315,3 +376,10 @@ func handleSignatureAuth(ctx context.Context, selector pool.Selectable[gatewayv1 return c.Authenticate(ctx, &authenticateRequest) } + +func handleOCMAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token string) (*gatewayv1beta1.AuthenticateResponse, error) { + return c.Authenticate(ctx, &gatewayv1beta1.AuthenticateRequest{ + Type: "ocmshares", + ClientId: token, + }) +} diff --git a/internal/http/services/owncloud/ocdav/net/net.go b/internal/http/services/owncloud/ocdav/net/net.go index 8d94ccd4ad..c0080a41d3 100644 --- a/internal/http/services/owncloud/ocdav/net/net.go +++ b/internal/http/services/owncloud/ocdav/net/net.go @@ -35,6 +35,7 @@ type ctxKey int const ( // CtxKeyBaseURI is the key of the base URI context field CtxKeyBaseURI ctxKey = iota + CtxOCM10 // NsDav is the Dav ns NsDav = "DAV:" diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index 840f6fe3e3..af7c7eb8ac 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -157,7 +157,7 @@ func (s *svc) Close() error { } func (s *svc) Unprotected() []string { - return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/"} + return []string{"/status.php", "/status", "/remote.php/dav/public-files/", "/apps/files/", "/index.php/f/", "/index.php/s/", "/remote.php/dav/ocm/", "/dav/ocm/"} } func (s *svc) Handler() http.Handler { diff --git a/internal/http/services/owncloud/ocs/config/config.go b/internal/http/services/owncloud/ocs/config/config.go index 9cb7883e68..01a815d80a 100644 --- a/internal/http/services/owncloud/ocs/config/config.go +++ b/internal/http/services/owncloud/ocs/config/config.go @@ -47,6 +47,9 @@ type Config struct { MachineAuthAPIKey string `mapstructure:"machine_auth_apikey"` SkipUpdatingExistingSharesMountpoints bool `mapstructure:"skip_updating_existing_shares_mountpoint"` EnableDenials bool `mapstructure:"enable_denials"` + OCMMountPoint string `mapstructure:"ocm_mount_point"` + ListOCMShares bool `mapstructure:"list_ocm_shares"` + Notifications map[string]interface{} `mapstructure:"notifications"` } // Init sets sane defaults diff --git a/internal/http/services/owncloud/ocs/conversions/main.go b/internal/http/services/owncloud/ocs/conversions/main.go index 7c2b6ac2ad..509068c746 100644 --- a/internal/http/services/owncloud/ocs/conversions/main.go +++ b/internal/http/services/owncloud/ocs/conversions/main.go @@ -26,6 +26,8 @@ import ( "path" "time" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/mime" "github.com/cs3org/reva/v2/pkg/publicshare" "github.com/cs3org/reva/v2/pkg/user" @@ -33,6 +35,7 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" publicsharemgr "github.com/cs3org/reva/v2/pkg/publicshare/manager/registry" @@ -303,6 +306,82 @@ func PublicShare2ShareData(share *link.PublicShare, r *http.Request, publicURL s return sd } +func formatRemoteUser(u *userpb.UserId) string { + return fmt.Sprintf("%s@%s", u.OpaqueId, u.Idp) +} + +func webdavInfo(protocols []*ocm.Protocol) (*ocm.WebDAVProtocol, bool) { + for _, p := range protocols { + if opt, ok := p.Term.(*ocm.Protocol_WebdavOptions); ok { + return opt.WebdavOptions, true + } + } + return nil, false +} + +// ReceivedOCMShare2ShareData converts a cs3 ocm received share into a share data model. +func ReceivedOCMShare2ShareData(share *ocm.ReceivedShare, path string) (*ShareData, error) { + webdav, ok := webdavInfo(share.Protocols) + if !ok { + return nil, errtypes.InternalError("webdav endpoint not in share") + } + + s := &ShareData{ + ID: share.Id.OpaqueId, + UIDOwner: formatRemoteUser(share.Creator), + UIDFileOwner: formatRemoteUser(share.Owner), + ShareWith: share.Grantee.GetUserId().OpaqueId, + Permissions: RoleFromResourcePermissions(webdav.GetPermissions().GetPermissions(), false).OCSPermissions(), + ShareType: ShareTypeFederatedCloudShare, + Path: path, + FileTarget: path, + MimeType: mime.Detect(share.ResourceType == provider.ResourceType_RESOURCE_TYPE_CONTAINER, share.Name), + ItemType: ResourceType(share.ResourceType).String(), + ItemSource: path, + STime: share.Ctime.Seconds, + Name: share.Name, + } + + if share.Expiration != nil { + s.Expiration = timestampToExpiration(share.Expiration) + } + return s, nil +} + +func webdavAMInfo(methods []*ocm.AccessMethod) (*ocm.WebDAVAccessMethod, bool) { + for _, a := range methods { + if opt, ok := a.Term.(*ocm.AccessMethod_WebdavOptions); ok { + return opt.WebdavOptions, true + } + } + return nil, false +} + +// OCMShare2ShareData converts a cs3 ocm share into a share data model. +func OCMShare2ShareData(share *ocm.Share) (*ShareData, error) { + webdav, ok := webdavAMInfo(share.AccessMethods) + if !ok { + return nil, errtypes.InternalError("webdav endpoint not in share") + } + + s := &ShareData{ + ID: share.Id.OpaqueId, + UIDOwner: share.Creator.OpaqueId, + UIDFileOwner: share.Owner.OpaqueId, + ShareWith: formatRemoteUser(share.Grantee.GetUserId()), + Permissions: RoleFromResourcePermissions(webdav.GetPermissions(), false).OCSPermissions(), + ShareType: ShareTypeFederatedCloudShare, + STime: share.Ctime.Seconds, + Name: share.Name, + } + + if share.Expiration != nil { + s.Expiration = timestampToExpiration(share.Expiration) + } + + return s, nil +} + // LocalUserIDToString transforms a cs3api user id into an ocs data model without domain name // TODO ocs uses user names ... so an additional lookup is needed. see mapUserIds() func LocalUserIDToString(userID *userpb.UserId) string { diff --git a/internal/http/services/owncloud/ocs/conversions/role.go b/internal/http/services/owncloud/ocs/conversions/role.go index eb42ca6077..6971957c8f 100644 --- a/internal/http/services/owncloud/ocs/conversions/role.go +++ b/internal/http/services/owncloud/ocs/conversions/role.go @@ -45,6 +45,8 @@ const ( RoleSpaceEditor = "spaceeditor" // RoleFileEditor grants editor permission on a single file. RoleFileEditor = "file-editor" + // RoleCoowner grants co-owner permissions on a resource. + RoleCoowner = "coowner" // RoleUploader grants uploader permission to upload onto a resource. RoleUploader = "uploader" // RoleManager grants manager permissions on a resource. Semantically equivalent to co-owner. @@ -286,6 +288,34 @@ func NewFileEditorRole() *Role { } } +// NewCoownerRole creates a coowner role. +func NewCoownerRole() *Role { + return &Role{ + Name: RoleCoowner, + cS3ResourcePermissions: &provider.ResourcePermissions{ + GetPath: true, + GetQuota: true, + InitiateFileDownload: true, + ListGrants: true, + ListContainer: true, + ListFileVersions: true, + ListRecycle: true, + Stat: true, + InitiateFileUpload: true, + RestoreFileVersion: true, + RestoreRecycleItem: true, + CreateContainer: true, + Delete: true, + Move: true, + PurgeRecycle: true, + AddGrant: true, + UpdateGrant: true, + RemoveGrant: true, + }, + ocsPermissions: PermissionAll, + } +} + // NewUploaderRole creates an uploader role func NewUploaderRole() *Role { return &Role{ diff --git a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go index cb4afe9d9b..14e8155146 100644 --- a/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go +++ b/internal/http/services/owncloud/ocs/handlers/apps/sharing/shares/remote.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,24 +19,28 @@ package shares import ( + "context" "net/http" - "strconv" + "path/filepath" + "strings" + providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" - ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" 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" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/go-chi/chi/v5" - "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response" + "github.com/cs3org/reva/v2/pkg/ocm/share" "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/go-chi/chi/v5" + "github.com/pkg/errors" ) -func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, statInfo *provider.ResourceInfo, role *conversions.Role, roleVal []byte) { +func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Request, resource *provider.ResourceInfo, role *conversions.Role, roleVal []byte) { ctx := r.Context() c, err := pool.GetGatewayServiceClient(h.gatewayAddr) @@ -51,7 +55,7 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque return } - providerInfoResp, err := c.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + providerInfoResp, err := c.GetInfoByDomain(ctx, &providerpb.GetInfoByDomainRequest{ Domain: shareWithProvider, }) if err != nil { @@ -59,8 +63,14 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque return } + if providerInfoResp.Status.Code != rpc.Code_CODE_OK { + // return proper error + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error from provider info response", errors.New(providerInfoResp.Status.Message)) + return + } + remoteUserRes, err := c.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ - RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_PRIMARY}, + RemoteUserId: &userpb.UserId{OpaqueId: shareWithUser, Idp: shareWithProvider, Type: userpb.UserType_USER_TYPE_FEDERATED}, }) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error searching recipient", err) @@ -71,39 +81,20 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque return } - createShareReq := &ocm.CreateOCMShareRequest{ - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - /* TODO extend the spec with role names? - "role": { - Decoder: "plain", - Value: []byte(role.Name), - }, - */ - "permissions": { - Decoder: "plain", - Value: []byte(strconv.Itoa(int(role.OCSPermissions()))), - }, - "name": { - Decoder: "plain", - Value: []byte(statInfo.Path), - }, - }, - }, - ResourceId: statInfo.Id, - Grant: &ocm.ShareGrant{ - Grantee: &provider.Grantee{ - Type: provider.GranteeType_GRANTEE_TYPE_USER, - Id: &provider.Grantee_UserId{UserId: remoteUserRes.RemoteUser.GetId()}, - }, - Permissions: &ocm.SharePermissions{ - Permissions: role.CS3ResourcePermissions(), + createShareResponse, err := c.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{ + ResourceId: resource.Id, + Grantee: &provider.Grantee{ + Type: provider.GranteeType_GRANTEE_TYPE_USER, + Id: &provider.Grantee_UserId{ + UserId: remoteUserRes.RemoteUser.Id, }, }, RecipientMeshProvider: providerInfoResp.ProviderInfo, - } - - createShareResponse, err := c.CreateOCMShare(ctx, createShareReq) + AccessMethods: []*ocm.AccessMethod{ + share.NewWebDavAccessMethod(role.CS3ResourcePermissions()), + share.NewWebappAccessMethod(getViewModeFromRole(role)), + }, + }) if err != nil { response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc create ocm share request", err) return @@ -117,12 +108,41 @@ func (h *Handler) createFederatedCloudShare(w http.ResponseWriter, r *http.Reque return } - response.WriteOCSSuccess(w, r, "OCM Share created") + s := createShareResponse.Share + data, err := conversions.OCMShare2ShareData(s) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error converting share", err) + return + } + h.mapUserIdsFederatedShare(ctx, c, data) + + info, status, err := h.getResourceInfoByID(ctx, c, s.ResourceId) + if err != nil { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", err) + return + } + if status.Code != rpc.Code_CODE_OK { + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error statting resource id", errors.New(status.Message)) + return + } + + h.addFileInfo(ctx, data, info) + + response.WriteOCSSuccess(w, r, data) } -// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid} -func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) { +func getViewModeFromRole(role *conversions.Role) providerv1beta1.ViewMode { + switch role.Name { + case conversions.RoleViewer: + return providerv1beta1.ViewMode_VIEW_MODE_READ_ONLY + case conversions.RoleEditor: + return providerv1beta1.ViewMode_VIEW_MODE_READ_WRITE + } + return providerv1beta1.ViewMode_VIEW_MODE_INVALID +} +// GetFederatedShare handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares/{shareid}. +func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) { // TODO: Implement response with HAL schemating ctx := r.Context() @@ -156,28 +176,125 @@ func (h *Handler) GetFederatedShare(w http.ResponseWriter, r *http.Request) { response.WriteOCSSuccess(w, r, share) } -// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares +// ListFederatedShares handles GET requests on /apps/files_sharing/api/v1/shares/remote_shares. func (h *Handler) ListFederatedShares(w http.ResponseWriter, r *http.Request) { - // TODO Implement pagination. // TODO Implement response with HAL schemating - ctx := r.Context() +} - gatewayClient, err := pool.GetGatewayServiceClient(h.gatewayAddr) +func (h *Handler) listReceivedFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, state ocm.ShareState) ([]*conversions.ShareData, error) { + listRes, err := gw.ListReceivedOCMShares(ctx, &ocm.ListReceivedOCMSharesRequest{}) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error getting grpc gateway client", err) - return + return nil, err } - listOCMSharesResponse, err := gatewayClient.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{}) + shares := []*conversions.ShareData{} + for _, s := range listRes.Shares { + if state != ocsStateUnknown && s.State != state { + continue + } + sd, err := conversions.ReceivedOCMShare2ShareData(s, h.ocmLocalMount(s)) + if err != nil { + continue + } + h.mapUserIdsReceivedFederatedShare(ctx, gw, sd) + sd.State = mapOCMState(s.State) + shares = append(shares, sd) + } + return shares, nil +} + +func (h *Handler) ocmLocalMount(share *ocm.ReceivedShare) string { + return filepath.Join("/", h.ocmMountPoint, share.Id.OpaqueId) +} + +func (h *Handler) mapUserIdsReceivedFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) { + if sd.ShareWith != "" { + user := h.mustGetIdentifiers(ctx, gw, sd.ShareWith, false) + sd.ShareWith = user.Username + sd.ShareWithDisplayname = user.DisplayName + } + + if sd.UIDOwner != "" { + user := h.mustGetRemoteUser(ctx, gw, sd.UIDOwner) + sd.DisplaynameOwner = user.DisplayName + } + + if sd.UIDFileOwner != "" { + user := h.mustGetRemoteUser(ctx, gw, sd.UIDFileOwner) + sd.DisplaynameFileOwner = user.DisplayName + } +} + +func (h *Handler) mapUserIdsFederatedShare(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, sd *conversions.ShareData) { + if sd.ShareWith != "" { + user := h.mustGetRemoteUser(ctx, gw, sd.ShareWith) + sd.ShareWith = user.Username + sd.ShareWithDisplayname = user.DisplayName + } + + if sd.UIDOwner != "" { + user := h.mustGetIdentifiers(ctx, gw, sd.UIDOwner, false) + sd.DisplaynameOwner = user.DisplayName + } + + if sd.UIDFileOwner != "" { + user := h.mustGetIdentifiers(ctx, gw, sd.UIDFileOwner, false) + sd.DisplaynameFileOwner = user.DisplayName + } +} + +func (h *Handler) mustGetRemoteUser(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, id string) *userIdentifiers { + s := strings.SplitN(id, "@", 2) + opaqueID, idp := s[0], s[1] + userRes, err := gw.GetAcceptedUser(ctx, &invitepb.GetAcceptedUserRequest{ + RemoteUserId: &userpb.UserId{ + Idp: idp, + OpaqueId: opaqueID, + }, + }) if err != nil { - response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error sending a grpc list ocm share request", err) - return + return &userIdentifiers{} + } + if userRes.Status.Code != rpc.Code_CODE_OK { + return &userIdentifiers{} + } + + user := userRes.RemoteUser + return &userIdentifiers{ + DisplayName: user.DisplayName, + Username: user.Username, + Mail: user.Mail, + } +} + +func (h *Handler) listOutcomingFederatedShares(ctx context.Context, gw gatewayv1beta1.GatewayAPIClient, filters []*ocm.ListOCMSharesRequest_Filter) ([]*conversions.ShareData, error) { + listRes, err := gw.ListOCMShares(ctx, &ocm.ListOCMSharesRequest{ + Filters: filters, + }) + if err != nil { + return nil, err } - shares := listOCMSharesResponse.GetShares() - if shares == nil { - shares = make([]*ocm.Share, 0) + shares := []*conversions.ShareData{} + for _, s := range listRes.Shares { + sd, err := conversions.OCMShare2ShareData(s) + if err != nil { + continue + } + h.mapUserIdsFederatedShare(ctx, gw, sd) + + info, status, err := h.getResourceInfoByID(ctx, gw, s.ResourceId) + if err != nil { + return nil, err + } + + if status.Code != rpc.Code_CODE_OK { + return nil, err + } + + h.addFileInfo(ctx, sd, info) + shares = append(shares, sd) } - response.WriteOCSSuccess(w, r, shares) + return shares, nil } 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 f2c0737bb2..4d3674f926 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 @@ -46,6 +46,7 @@ import ( "google.golang.org/grpc/metadata" "google.golang.org/protobuf/types/known/fieldmaskpb" + ocmv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/config" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/response" @@ -82,6 +83,8 @@ type Handler struct { publicURL string sharePrefix string homeNamespace string + ocmMountPoint string + listOCMShares bool skipUpdatingExistingSharesMountpoints bool additionalInfoTemplate *template.Template userIdentifierCache *ttlcache.Cache @@ -132,6 +135,8 @@ func (h *Handler) Init(c *config.Config) error { h.publicURL = c.Config.Host h.sharePrefix = c.SharePrefix h.homeNamespace = c.HomeNamespace + h.ocmMountPoint = c.OCMMountPoint + h.listOCMShares = c.ListOCMShares h.skipUpdatingExistingSharesMountpoints = c.SkipUpdatingExistingSharesMountpoints h.additionalInfoTemplate, _ = template.New("additionalInfo").Parse(c.AdditionalInfoAttribute) @@ -861,7 +866,8 @@ const ( func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) { // which pending state to list - stateFilter := getStateFilter(r.FormValue("state")) + state := r.FormValue("state") + stateFilter := getStateFilter(state) showHidden, _ := strconv.ParseBool(r.URL.Query().Get("show_hidden")) @@ -1037,6 +1043,18 @@ func (h *Handler) listSharesWithMe(w http.ResponseWriter, r *http.Request) { sublog.Debug().Msgf("share: %+v", *data) } + if h.listOCMShares { + // include ocm shares in the response + stateFilter := getOCMStateFilter(state) + lst, err := h.listReceivedFederatedShares(ctx, client, stateFilter) + if err != nil { + sublog.Err(err).Msg("error listing received ocm shares") + response.WriteOCSError(w, r, response.MetaServerError.StatusCode, "error listing received ocm shares", err) + return + } + shares = append(shares, lst...) + } + response.WriteOCSSuccess(w, r, shares) } @@ -1520,6 +1538,19 @@ func mapState(state collaboration.ShareState) int { return mapped } +func mapOCMState(state ocmv1beta1.ShareState) int { + switch state { + case ocmv1beta1.ShareState_SHARE_STATE_PENDING: + return ocsStatePending + case ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED: + return ocsStateAccepted + case ocmv1beta1.ShareState_SHARE_STATE_REJECTED: + return ocsStateRejected + default: + return ocsStateUnknown + } +} + func getStateFilter(s string) collaboration.ShareState { var stateFilter collaboration.ShareState switch s { @@ -1537,6 +1568,21 @@ func getStateFilter(s string) collaboration.ShareState { return stateFilter } +func getOCMStateFilter(s string) ocmv1beta1.ShareState { + switch s { + case "all": + return ocsStateUnknown // no filter + case "0": // accepted + return ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED + case "1": // pending + return ocmv1beta1.ShareState_SHARE_STATE_PENDING + case "2": // rejected + return ocmv1beta1.ShareState_SHARE_STATE_REJECTED + default: + return ocmv1beta1.ShareState_SHARE_STATE_ACCEPTED + } +} + func (h *Handler) getPoolClient() (gateway.GatewayAPIClient, error) { return pool.GetGatewayServiceClient(h.gatewayAddr) } 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 14037c3510..8b535be7f7 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 @@ -25,6 +25,7 @@ import ( userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" @@ -252,7 +253,46 @@ func (h *Handler) listUserShares(r *http.Request, filters []*collaboration.Filte log.Debug().Interface("share", s).Interface("info", info).Interface("shareData", data).Msg("mapped") ocsDataPayload = append(ocsDataPayload, data) } + + if h.listOCMShares { + // include the ocm shares + ocmShares, err := h.listOutcomingFederatedShares(ctx, client, convertToOCMFilters(filters)) + if err != nil { + return nil, nil, err + } + ocsDataPayload = append(ocsDataPayload, ocmShares...) + } } return ocsDataPayload, nil, nil } + +func convertToOCMFilters(filters []*collaboration.Filter) []*ocmpb.ListOCMSharesRequest_Filter { + ocmfilters := []*ocmpb.ListOCMSharesRequest_Filter{} + for _, f := range filters { + switch v := f.Term.(type) { + case *collaboration.Filter_ResourceId: + ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{ + Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_RESOURCE_ID, + Term: &ocmpb.ListOCMSharesRequest_Filter_ResourceId{ + ResourceId: v.ResourceId, + }, + }) + case *collaboration.Filter_Creator: + ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{ + Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_CREATOR, + Term: &ocmpb.ListOCMSharesRequest_Filter_Creator{ + Creator: v.Creator, + }, + }) + case *collaboration.Filter_Owner: + ocmfilters = append(ocmfilters, &ocmpb.ListOCMSharesRequest_Filter{ + Type: ocmpb.ListOCMSharesRequest_Filter_TYPE_OWNER, + Term: &ocmpb.ListOCMSharesRequest_Filter_Owner{ + Owner: v.Owner, + }, + }) + } + } + return ocmfilters +} diff --git a/internal/http/services/ocmd/reqres.go b/internal/http/services/reqres/reqres.go similarity index 87% rename from internal/http/services/ocmd/reqres.go rename to internal/http/services/reqres/reqres.go index f0fd55be9e..acd0ee822f 100644 --- a/internal/http/services/ocmd/reqres.go +++ b/internal/http/services/reqres/reqres.go @@ -1,4 +1,4 @@ -// Copyright 2018-2021 CERN +// Copyright 2018-2023 CERN // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,7 +16,7 @@ // granted to it by virtue of its status as an Intergovernmental Organization // or submit itself to any jurisdiction. -package ocmd +package reqres import ( "encoding/json" @@ -25,10 +25,10 @@ import ( "github.com/cs3org/reva/v2/pkg/appctx" ) -// APIErrorCode stores the type of error encountered +// APIErrorCode stores the type of error encountered. type APIErrorCode string -// The various types of errors that can be expected to occur +// The various types of errors that can be expected to occur. const ( APIErrorNotFound APIErrorCode = "RESOURCE_NOT_FOUND" APIErrorUnauthenticated APIErrorCode = "UNAUTHENTICATED" @@ -36,10 +36,11 @@ const ( APIErrorUnimplemented APIErrorCode = "FUNCTION_NOT_IMPLEMENTED" APIErrorInvalidParameter APIErrorCode = "INVALID_PARAMETER" APIErrorProviderError APIErrorCode = "PROVIDER_ERROR" + APIErrorAlreadyExist APIErrorCode = "ALREADY_EXIST" APIErrorServerError APIErrorCode = "SERVER_ERROR" ) -// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes +// APIErrorCodeMapping stores the HTTP error code mapping for various APIErrorCodes. var APIErrorCodeMapping = map[APIErrorCode]int{ APIErrorNotFound: http.StatusNotFound, APIErrorUnauthenticated: http.StatusUnauthorized, @@ -47,16 +48,17 @@ var APIErrorCodeMapping = map[APIErrorCode]int{ APIErrorUnimplemented: http.StatusNotImplemented, APIErrorInvalidParameter: http.StatusBadRequest, APIErrorProviderError: http.StatusBadGateway, + APIErrorAlreadyExist: http.StatusConflict, APIErrorServerError: http.StatusInternalServerError, } -// APIError encompasses the error type and message +// APIError encompasses the error type and message. type APIError struct { Code APIErrorCode `json:"code"` Message string `json:"message"` } -// WriteError handles writing error responses +// WriteError handles writing error responses. func WriteError(w http.ResponseWriter, r *http.Request, code APIErrorCode, message string, e error) { if e != nil { appctx.GetLogger(r.Context()).Error().Err(e).Msg(message) diff --git a/internal/http/services/sciencemesh/apps.go b/internal/http/services/sciencemesh/apps.go new file mode 100644 index 0000000000..2c4bf356c1 --- /dev/null +++ b/internal/http/services/sciencemesh/apps.go @@ -0,0 +1,139 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "strings" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocmpb "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/reqres" + "github.com/cs3org/reva/v2/pkg/errtypes" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/rhttp/router" +) + +type appsHandler struct { + gatewayClient gateway.GatewayAPIClient + ocmMountPoint string +} + +func (h *appsHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc) + if err != nil { + return err + } + h.ocmMountPoint = c.OCMMountPoint + + return nil +} + +func (h *appsHandler) shareInfo(p string) (*ocmpb.ShareId, string) { + p = strings.TrimPrefix(p, h.ocmMountPoint) + shareID, rel := router.ShiftPath(p) + if len(rel) > 0 { + rel = rel[1:] + } + return &ocmpb.ShareId{OpaqueId: shareID}, rel +} + +func (h *appsHandler) OpenInApp(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + if err := r.ParseForm(); err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "parameters could not be parsed", nil) + return + } + + path := r.Form.Get("file") + if path == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing file", nil) + return + } + + shareID, rel := h.shareInfo(path) + + template, err := h.webappTemplate(ctx, shareID) + if err != nil { + var e errtypes.NotFound + if errors.As(err, &e) { + reqres.WriteError(w, r, reqres.APIErrorNotFound, e.Error(), nil) + } + reqres.WriteError(w, r, reqres.APIErrorServerError, err.Error(), err) + return + } + + url := resolveTemplate(template, rel) + + if err := json.NewEncoder(w).Encode(map[string]any{ + "app_url": url, + }); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling JSON response", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +func (h *appsHandler) webappTemplate(ctx context.Context, id *ocmpb.ShareId) (string, error) { + res, err := h.gatewayClient.GetReceivedOCMShare(ctx, &ocmpb.GetReceivedOCMShareRequest{ + Ref: &ocmpb.ShareReference{ + Spec: &ocmpb.ShareReference_Id{ + Id: id, + }, + }, + }) + if err != nil { + return "", err + } + if res.Status.Code != rpcv1beta1.Code_CODE_OK { + if res.Status.Code == rpcv1beta1.Code_CODE_NOT_FOUND { + return "", errtypes.NotFound(res.Status.Message) + } + return "", errtypes.InternalError(res.Status.Message) + } + + webapp, ok := getWebappProtocol(res.Share.Protocols) + if !ok { + return "", errtypes.BadRequest("share does not contain webapp protocol") + } + + return webapp.UriTemplate, nil +} + +func getWebappProtocol(protocols []*ocmpb.Protocol) (*ocmpb.WebappProtocol, bool) { + for _, p := range protocols { + if t, ok := p.Term.(*ocmpb.Protocol_WebappOptions); ok { + return t.WebappOptions, true + } + } + return nil, false +} + +func resolveTemplate(template string, rel string) string { + // the template is of type "https://open-cloud-mesh.org/s/share-hash/{relative-path-to-shared-resource}" + return strings.Replace(template, "{relative-path-to-shared-resource}", rel, 1) +} diff --git a/internal/http/services/sciencemesh/email.go b/internal/http/services/sciencemesh/email.go new file mode 100644 index 0000000000..239c89ba5d --- /dev/null +++ b/internal/http/services/sciencemesh/email.go @@ -0,0 +1,119 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "bytes" + "html/template" + "io" + "os" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" +) + +type emailParams struct { + User *userpb.User + Token string + MeshDirectoryURL string + InviteLink string +} + +const defaultSubject = `ScienceMesh: {{.User.DisplayName}} wants to collaborate with you` + +const defaultBody = `Hi + +{{.User.DisplayName}} ({{.User.Mail}}) wants to start sharing OCM resources with you. +To accept the invite, please visit the following URL: +{{.InviteLink}} + +Alternatively, you can visit your mesh provider and use the following details: +Token: {{.Token}} +ProviderDomain: {{.User.Id.Idp}} + +Best, +The ScienceMesh team` + +func (h *tokenHandler) sendEmail(recipient string, obj *emailParams) error { + subj, err := h.generateEmailSubject(obj) + if err != nil { + return err + } + + body, err := h.generateEmailBody(obj) + if err != nil { + return err + } + + return h.smtpCredentials.SendMail(recipient, subj, body) +} + +func (h *tokenHandler) generateEmailSubject(obj *emailParams) (string, error) { + var buf bytes.Buffer + err := h.tplSubj.Execute(&buf, obj) + return buf.String(), err +} + +func (h *tokenHandler) generateEmailBody(obj *emailParams) (string, error) { + var buf bytes.Buffer + err := h.tplBody.Execute(&buf, obj) + return buf.String(), err +} + +func (h *tokenHandler) initBodyTemplate(bodyTemplPath string) error { + var body string + if bodyTemplPath == "" { + body = defaultBody + } else { + f, err := os.Open(bodyTemplPath) + if err != nil { + return err + } + defer f.Close() + + data, err := io.ReadAll(f) + if err != nil { + return err + } + body = string(data) + } + + tpl, err := template.New("tpl_body").Parse(body) + if err != nil { + return err + } + + h.tplBody = tpl + return nil +} + +func (h *tokenHandler) initSubjectTemplate(subjTempl string) error { + var subj string + if subjTempl == "" { + subj = defaultSubject + } else { + subj = subjTempl + } + + tpl, err := template.New("tpl_subj").Parse(subj) + if err != nil { + return err + } + h.tplSubj = tpl + return nil +} diff --git a/internal/http/services/sciencemesh/providers.go b/internal/http/services/sciencemesh/providers.go new file mode 100644 index 0000000000..72d8515d4a --- /dev/null +++ b/internal/http/services/sciencemesh/providers.go @@ -0,0 +1,87 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "encoding/json" + "errors" + "net/http" + "strings" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/reqres" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" +) + +type providersHandler struct { + gatewayClient gateway.GatewayAPIClient +} + +func (h *providersHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc) + if err != nil { + return err + } + + return nil +} + +type provider struct { + FullName string `json:"full_name"` + Domain string `json:"domain"` +} + +// ListProviders lists all the providers filtering by the `search` query parameter. +func (h *providersHandler) ListProviders(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + term := strings.ToLower(r.URL.Query().Get("search")) + + listRes, err := h.gatewayClient.ListAllProviders(ctx, &providerpb.ListAllProvidersRequest{}) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing all providers", err) + return + } + + if listRes.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorServerError, listRes.Status.Message, errors.New(listRes.Status.Message)) + return + } + + filtered := []*provider{} + for _, p := range listRes.Providers { + if strings.Contains(strings.ToLower(p.FullName), term) || + strings.Contains(strings.ToLower(p.Domain), term) { + filtered = append(filtered, &provider{ + FullName: p.FullName, + Domain: p.Domain, + }) + } + } + + if err := json.NewEncoder(w).Encode(filtered); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error encoding response in json", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} diff --git a/internal/http/services/sciencemesh/sciencemesh.go b/internal/http/services/sciencemesh/sciencemesh.go new file mode 100644 index 0000000000..67b2f902a9 --- /dev/null +++ b/internal/http/services/sciencemesh/sciencemesh.go @@ -0,0 +1,136 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "net/http" + + "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/rhttp/global" + "github.com/cs3org/reva/v2/pkg/sharedconf" + "github.com/cs3org/reva/v2/pkg/smtpclient" + "github.com/cs3org/reva/v2/pkg/utils/cfg" + "github.com/go-chi/chi/v5" + "github.com/rs/zerolog" +) + +func init() { + global.Register("sciencemesh", New) +} + +// New returns a new sciencemesh service. +func New(m map[string]interface{}, _ *zerolog.Logger) (global.Service, error) { + var c config + if err := cfg.Decode(m, &c); err != nil { + return nil, err + } + + r := chi.NewRouter() + s := &svc{ + conf: &c, + router: r, + } + + if err := s.routerInit(); err != nil { + return nil, err + } + + return s, nil +} + +// Close performs cleanup. +func (s *svc) Close() error { + return nil +} + +type config struct { + Prefix string `mapstructure:"prefix"` + SMTPCredentials *smtpclient.SMTPCredentials `mapstructure:"smtp_credentials" validate:"required"` + GatewaySvc string `mapstructure:"gatewaysvc" validate:"required"` + MeshDirectoryURL string `mapstructure:"mesh_directory_url" validate:"required"` + ProviderDomain string `mapstructure:"provider_domain" validate:"required"` + SubjectTemplate string `mapstructure:"subject_template"` + BodyTemplatePath string `mapstructure:"body_template_path"` + OCMMountPoint string `mapstructure:"ocm_mount_point"` +} + +func (c *config) ApplyDefaults() { + if c.Prefix == "" { + c.Prefix = "sciencemesh" + } + if c.OCMMountPoint == "" { + c.OCMMountPoint = "/ocm" + } + + c.GatewaySvc = sharedconf.GetGatewaySVC(c.GatewaySvc) +} + +type svc struct { + conf *config + router chi.Router +} + +func (s *svc) routerInit() error { + tokenHandler := new(tokenHandler) + if err := tokenHandler.init(s.conf); err != nil { + return err + } + providersHandler := new(providersHandler) + if err := providersHandler.init(s.conf); err != nil { + return err + } + sharesHandler := new(sharesHandler) + if err := sharesHandler.init(s.conf); err != nil { + return err + } + + appsHandler := new(appsHandler) + if err := appsHandler.init(s.conf); err != nil { + return err + } + + s.router.Get("/generate-invite", tokenHandler.Generate) + s.router.Get("/list-invite", tokenHandler.ListInvite) + s.router.Post("/accept-invite", tokenHandler.AcceptInvite) + s.router.Get("/find-accepted-users", tokenHandler.FindAccepted) + s.router.Delete("/delete-accepted-user", tokenHandler.DeleteAccepted) + s.router.Get("/list-providers", providersHandler.ListProviders) + s.router.Post("/create-share", sharesHandler.CreateShare) + s.router.Post("/open-in-app", appsHandler.OpenInApp) + return nil +} + +func (s *svc) Prefix() string { + return s.conf.Prefix +} + +func (s *svc) Unprotected() []string { + return nil +} + +func (s *svc) Handler() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + log.Debug().Str("path", r.URL.Path).Msg("sciencemesh routing") + + // unset raw path, otherwise chi uses it to route and then fails to match percent encoded path segments + r.URL.RawPath = "" + s.router.ServeHTTP(w, r) + }) +} diff --git a/internal/http/services/sciencemesh/share.go b/internal/http/services/sciencemesh/share.go new file mode 100644 index 0000000000..efb6f3d4a7 --- /dev/null +++ b/internal/http/services/sciencemesh/share.go @@ -0,0 +1,179 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "encoding/json" + "errors" + "mime" + "net/http" + + appprovider "github.com/cs3org/go-cs3apis/cs3/app/provider/v1beta1" + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + ocm "github.com/cs3org/go-cs3apis/cs3/sharing/ocm/v1beta1" + providerpb "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/owncloud/ocs/conversions" + "github.com/cs3org/reva/v2/internal/http/services/reqres" + "github.com/cs3org/reva/v2/pkg/appctx" + "github.com/cs3org/reva/v2/pkg/ocm/share" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/go-playground/validator/v10" +) + +var validate = validator.New() + +type sharesHandler struct { + gatewayClient gateway.GatewayAPIClient +} + +func (h *sharesHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc) + return err +} + +type createShareRequest struct { + SourcePath string `json:"sourcePath" validate:"required"` + TargetPath string `json:"targetPath" validate:"required"` + Type string `json:"type"` + Role string `json:"role" validate:"oneof=viewer editor"` + RecipientUsername string `json:"recipientUsername" validate:"required"` + RecipientHost string `json:"recipientHost" validate:"required"` +} + +// CreateShare creates an OCM share. +func (h *sharesHandler) CreateShare(w http.ResponseWriter, r *http.Request) { + log := appctx.GetLogger(r.Context()) + + req, err := getCreateShareRequest(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "invalid parameters", err) + return + } + + ctx := r.Context() + + statRes, err := h.gatewayClient.Stat(ctx, &providerpb.StatRequest{ + Ref: &providerpb.Reference{ + Path: req.SourcePath, + }, + }) + switch { + case err != nil: + reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error", err) + return + case statRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + reqres.WriteError(w, r, reqres.APIErrorNotFound, statRes.Status.Message, nil) + return + case statRes.Status.Code != rpc.Code_CODE_OK: + reqres.WriteError(w, r, reqres.APIErrorServerError, statRes.Status.Message, errors.New(statRes.Status.Message)) + return + } + + recipientProviderInfo, err := h.gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + Domain: req.RecipientHost, + }) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err) + return + } + if recipientProviderInfo.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorNotFound, recipientProviderInfo.Status.Message, errors.New(recipientProviderInfo.Status.Message)) + return + } + + perm, viewMode := getPermissionsByRole(req.Role) + + log.Debug().Msg("calling gatewayClient.CreateOCMShare from sciencemesh/share.go") + shareRes, err := h.gatewayClient.CreateOCMShare(ctx, &ocm.CreateOCMShareRequest{ + ResourceId: statRes.Info.Id, + Grantee: &providerpb.Grantee{ + Type: providerpb.GranteeType_GRANTEE_TYPE_USER, + Id: &providerpb.Grantee_UserId{ + UserId: &userpb.UserId{ + Idp: req.RecipientHost, + OpaqueId: req.RecipientUsername, + }, + }, + }, + RecipientMeshProvider: recipientProviderInfo.ProviderInfo, + AccessMethods: []*ocm.AccessMethod{ + share.NewWebDavAccessMethod(perm), + share.NewWebappAccessMethod(viewMode), + }, + }) + log.Debug().Msg("called gatewayClient.CreateOCMShare from sciencemesh/share.go") + + switch { + case err != nil: + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc CreateOCMShare", err) + return + case shareRes.Status.Code == rpc.Code_CODE_NOT_FOUND: + reqres.WriteError(w, r, reqres.APIErrorNotFound, shareRes.Status.Message, nil) + return + case shareRes.Status.Code == rpc.Code_CODE_ALREADY_EXISTS: + reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, shareRes.Status.Message, nil) + return + case shareRes.Status.Code != rpc.Code_CODE_OK: + reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, shareRes.Status.Message, errors.New(shareRes.Status.Message)) + return + } + + if err := json.NewEncoder(w).Encode(shareRes); err != nil { + log.Error().Err(err).Msg("error encoding response") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} + +func getPermissionsByRole(role string) (*providerpb.ResourcePermissions, appprovider.ViewMode) { + switch role { + case "viewer": + return conversions.NewViewerRole(false).CS3ResourcePermissions(), appprovider.ViewMode_VIEW_MODE_READ_ONLY + case "editor": + return conversions.NewEditorRole(false).CS3ResourcePermissions(), appprovider.ViewMode_VIEW_MODE_READ_WRITE + } + return nil, 0 +} + +func getCreateShareRequest(r *http.Request) (*createShareRequest, error) { + var req createShareRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + return nil, errors.New("body request not recognised") + } + // set defaults + if req.Type == "" { + req.Type = "viewer" + } + // validate the request + if err := validate.Struct(req); err != nil { + return nil, err + } + return &req, nil +} diff --git a/internal/http/services/sciencemesh/token.go b/internal/http/services/sciencemesh/token.go new file mode 100644 index 0000000000..946aa97430 --- /dev/null +++ b/internal/http/services/sciencemesh/token.go @@ -0,0 +1,324 @@ +// Copyright 2018-2023 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package sciencemesh + +import ( + "encoding/json" + "errors" + "html/template" + "mime" + "net/http" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + invitepb "github.com/cs3org/go-cs3apis/cs3/ocm/invite/v1beta1" + ocmprovider "github.com/cs3org/go-cs3apis/cs3/ocm/provider/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/cs3org/reva/v2/internal/http/services/reqres" + "github.com/cs3org/reva/v2/pkg/appctx" + ctxpkg "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool" + "github.com/cs3org/reva/v2/pkg/smtpclient" + "github.com/cs3org/reva/v2/pkg/utils/list" +) + +type tokenHandler struct { + gatewayClient gateway.GatewayAPIClient + smtpCredentials *smtpclient.SMTPCredentials + meshDirectoryURL string + providerDomain string + tplSubj *template.Template + tplBody *template.Template +} + +func (h *tokenHandler) init(c *config) error { + var err error + h.gatewayClient, err = pool.GetGatewayServiceClient(c.GatewaySvc) + if err != nil { + return err + } + + if c.SMTPCredentials != nil { + h.smtpCredentials = smtpclient.NewSMTPCredentials(c.SMTPCredentials) + } + + h.meshDirectoryURL = c.MeshDirectoryURL + h.providerDomain = c.ProviderDomain + + if err := h.initSubjectTemplate(c.SubjectTemplate); err != nil { + return err + } + + if err := h.initBodyTemplate(c.BodyTemplatePath); err != nil { + return err + } + + return nil +} + +type token struct { + Token string `json:"token"` + Description string `json:"description,omitempty"` + Expiration uint64 `json:"expiration,omitempty"` + InviteLink string `json:"invite_link"` +} + +// Generate generates an invitation token and if a recipient is specified, +// will send an email containing the link the user will use to accept the +// invitation. +func (h *tokenHandler) Generate(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + query := r.URL.Query() + token, err := h.gatewayClient.GenerateInviteToken(ctx, &invitepb.GenerateInviteTokenRequest{ + Description: query.Get("description"), + }) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error generating token", err) + return + } + + user := ctxpkg.ContextMustGetUser(ctx) + recipient := query.Get("recipient") + if recipient != "" && h.smtpCredentials != nil { + templObj := &emailParams{ + User: user, + Token: token.InviteToken.Token, + MeshDirectoryURL: h.meshDirectoryURL, + } + if err := h.sendEmail(recipient, templObj); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending token by mail", err) + return + } + } + + tknRes := h.prepareGenerateTokenResponse(token.InviteToken) + if err := json.NewEncoder(w).Encode(tknRes); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +func (h *tokenHandler) prepareGenerateTokenResponse(tkn *invitepb.InviteToken) *token { + res := &token{ + Token: tkn.Token, + Description: tkn.Description, + InviteLink: h.meshDirectoryURL + "?token=" + tkn.Token + "&providerDomain=" + h.providerDomain, + } + if tkn.Expiration != nil { + res.Expiration = tkn.Expiration.Seconds + } + + return res +} + +type acceptInviteRequest struct { + Token string `json:"token"` + ProviderDomain string `json:"providerDomain"` +} + +// AcceptInvite accepts an invitation from the user in the remote provider. +func (h *tokenHandler) AcceptInvite(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + log := appctx.GetLogger(ctx) + + req, err := getAcceptInviteRequest(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err) + return + } + + if req.Token == "" || req.ProviderDomain == "" { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token and providerDomain must not be null", nil) + return + } + + providerInfo, err := h.gatewayClient.GetInfoByDomain(ctx, &ocmprovider.GetInfoByDomainRequest{ + Domain: req.ProviderDomain, + }) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err) + return + } + if providerInfo.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorServerError, "grpc forward invite request failed", errors.New(providerInfo.Status.Message)) + return + } + + forwardInviteReq := &invitepb.ForwardInviteRequest{ + InviteToken: &invitepb.InviteToken{ + Token: req.Token, + }, + OriginSystemProvider: providerInfo.ProviderInfo, + } + forwardInviteResponse, err := h.gatewayClient.ForwardInvite(ctx, forwardInviteReq) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc forward invite request", err) + return + } + if forwardInviteResponse.Status.Code != rpc.Code_CODE_OK { + switch forwardInviteResponse.Status.Code { + case rpc.Code_CODE_NOT_FOUND: + reqres.WriteError(w, r, reqres.APIErrorNotFound, "token not found", nil) + return + case rpc.Code_CODE_INVALID_ARGUMENT: + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "token has expired", nil) + return + case rpc.Code_CODE_ALREADY_EXISTS: + reqres.WriteError(w, r, reqres.APIErrorAlreadyExist, "user already known", nil) + return + case rpc.Code_CODE_PERMISSION_DENIED: + reqres.WriteError(w, r, reqres.APIErrorUnauthenticated, "remove service not trusted", nil) + return + default: + reqres.WriteError(w, r, reqres.APIErrorServerError, "unexpected error: "+forwardInviteResponse.Status.Message, errors.New(forwardInviteResponse.Status.Message)) + return + } + } + + w.WriteHeader(http.StatusOK) + + log.Info().Str("token", req.Token).Str("provider", req.ProviderDomain).Msgf("invite forwarded") +} + +func getAcceptInviteRequest(r *http.Request) (*acceptInviteRequest, error) { + var req acceptInviteRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + req.Token, req.ProviderDomain = r.FormValue("token"), r.FormValue("providerDomain") + } + return &req, nil +} + +type remoteUser struct { + DisplayName string `json:"display_name"` + Idp string `json:"idp"` + UserID string `json:"user_id"` + Mail string `json:"mail"` +} + +// FindAccepted returns the list of all the users that accepted the invitation +// to the authenticated user. +func (h *tokenHandler) FindAccepted(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + res, err := h.gatewayClient.FindAcceptedUsers(ctx, &invitepb.FindAcceptedUsersRequest{}) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc find accepted users request", err) + return + } + + users := list.Map(res.AcceptedUsers, func(u *userpb.User) *remoteUser { + return &remoteUser{ + DisplayName: u.DisplayName, + Idp: u.Id.Idp, + UserID: u.Id.OpaqueId, + Mail: u.Mail, + } + }) + + if err := json.NewEncoder(w).Encode(users); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} + +// DeleteAccepted deletes the given user from the list of the accepted users. +func (h *tokenHandler) DeleteAccepted(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + req, err := getDeleteAcceptedRequest(r) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorInvalidParameter, "missing parameters in request", err) + return + } + + res, err := h.gatewayClient.DeleteAcceptedUser(ctx, &invitepb.DeleteAcceptedUserRequest{ + RemoteUserId: &userpb.UserId{ + Idp: req.Idp, + OpaqueId: req.UserID, + Type: userpb.UserType_USER_TYPE_FEDERATED, + }, + }) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error sending a grpc get invite by domain info request", err) + return + } + if res.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorServerError, "grpc forward invite request failed", errors.New(res.Status.Message)) + return + } + w.WriteHeader(http.StatusOK) +} + +type deleteAcceptedRequest struct { + Idp string `json:"idp"` + UserID string `json:"user_id"` +} + +func getDeleteAcceptedRequest(r *http.Request) (*deleteAcceptedRequest, error) { + var req deleteAcceptedRequest + contentType, _, err := mime.ParseMediaType(r.Header.Get("Content-Type")) + if err == nil && contentType == "application/json" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + return nil, err + } + } else { + req.Idp, req.UserID = r.FormValue("idp"), r.FormValue("user_id") + } + return &req, nil +} + +func (h *tokenHandler) ListInvite(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + res, err := h.gatewayClient.ListInviteTokens(ctx, &invitepb.ListInviteTokensRequest{}) + if err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error listing tokens", err) + return + } + + if res.Status.Code != rpc.Code_CODE_OK { + reqres.WriteError(w, r, reqres.APIErrorServerError, res.Status.Message, errors.New(res.Status.Message)) + return + } + + tokens := make([]*token, 0, len(res.InviteTokens)) + for _, tkn := range res.InviteTokens { + tokens = append(tokens, h.prepareGenerateTokenResponse(tkn)) + } + + if err := json.NewEncoder(w).Encode(tokens); err != nil { + reqres.WriteError(w, r, reqres.APIErrorServerError, "error marshalling token data", err) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) +} diff --git a/pkg/app/app.go b/pkg/app/app.go index 50e6ce28d8..ab672888e3 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -40,6 +40,6 @@ type Registry interface { // Provider is the interface that application providers implement // for interacting with external apps that serve the requested resource. type Provider interface { - GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error) + GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) GetAppProviderInfo(ctx context.Context) (*registry.ProviderInfo, error) } diff --git a/pkg/app/provider/demo/demo.go b/pkg/app/provider/demo/demo.go index 0b00fa419a..9964eb0948 100644 --- a/pkg/app/provider/demo/demo.go +++ b/pkg/app/provider/demo/demo.go @@ -39,7 +39,7 @@ type demoProvider struct { iframeUIProvider string } -func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.OpenInAppRequest_ViewMode, token, language string) (*appprovider.OpenInAppURL, error) { +func (p *demoProvider) GetAppURL(ctx context.Context, resource *provider.ResourceInfo, viewMode appprovider.ViewMode, token, language string) (*appprovider.OpenInAppURL, error) { url := fmt.Sprintf("