From e053d40dc005ed26432f04ac769b325bfc0c88a6 Mon Sep 17 00:00:00 2001 From: Michiel de Jong Date: Mon, 20 Sep 2021 15:15:34 +0200 Subject: [PATCH] Nextcloud-based User and Auth managers --- .../unreleased/nextcloud-user-backend.md | 5 + pkg/auth/manager/nextcloud/nextcloud.go | 66 ++++-- .../nextcloud/nextcloud_server_mock.go | 107 +++++++++ .../manager/nextcloud/nextcloud_suite_test.go | 31 +++ pkg/auth/manager/nextcloud/nextcloud_test.go | 136 +++++++++++ pkg/user/manager/nextcloud/nextcloud.go | 136 +++++++---- .../nextcloud/nextcloud_server_mock.go | 110 +++++++++ .../manager/nextcloud/nextcloud_suite_test.go | 31 +++ pkg/user/manager/nextcloud/nextcloud_test.go | 214 ++++++++++++++++++ 9 files changed, 769 insertions(+), 67 deletions(-) create mode 100644 changelog/unreleased/nextcloud-user-backend.md create mode 100644 pkg/auth/manager/nextcloud/nextcloud_server_mock.go create mode 100644 pkg/auth/manager/nextcloud/nextcloud_suite_test.go create mode 100644 pkg/auth/manager/nextcloud/nextcloud_test.go create mode 100644 pkg/user/manager/nextcloud/nextcloud_server_mock.go create mode 100644 pkg/user/manager/nextcloud/nextcloud_suite_test.go create mode 100644 pkg/user/manager/nextcloud/nextcloud_test.go diff --git a/changelog/unreleased/nextcloud-user-backend.md b/changelog/unreleased/nextcloud-user-backend.md new file mode 100644 index 0000000000..6e8afd7421 --- /dev/null +++ b/changelog/unreleased/nextcloud-user-backend.md @@ -0,0 +1,5 @@ +Enhancement: More unit tests for the Nextcloud auth and user managers + +Adds more unit tests for the Nextcloud auth manager and the Nextcloud user manager + +https://github.com/cs3org/reva/pull/2087 diff --git a/pkg/auth/manager/nextcloud/nextcloud.go b/pkg/auth/manager/nextcloud/nextcloud.go index 98889346c5..c355f4a5a8 100644 --- a/pkg/auth/manager/nextcloud/nextcloud.go +++ b/pkg/auth/manager/nextcloud/nextcloud.go @@ -44,7 +44,8 @@ type mgr struct { endPoint string } -type config struct { +// AuthManagerConfig contains config for a Nextcloud-based AuthManager +type AuthManagerConfig struct { EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user check"` } @@ -55,11 +56,11 @@ type Action struct { argS string } -func (c *config) init() { +func (c *AuthManagerConfig) init() { } -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} +func parseConfig(m map[string]interface{}) (*AuthManagerConfig, error) { + c := &AuthManagerConfig{} if err := mapstructure.Decode(m, c); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err @@ -75,9 +76,14 @@ func New(m map[string]interface{}) (auth.Manager, error) { } c.init() + return NewAuthManager(c, &http.Client{}) +} + +// NewAuthManager returns a new Nextcloud-based AuthManager +func NewAuthManager(c *AuthManagerConfig, hc *http.Client) (auth.Manager, error) { return &mgr{ endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" - client: &http.Client{}, + client: hc, }, nil } @@ -92,7 +98,7 @@ func (am *mgr) do(ctx context.Context, a Action) (int, []byte, error) { // return 0, nil, err // } // url := am.endPoint + "~" + a.username + "/api/" + a.verb - url := "http://localhost/apps/sciencemesh/~" + a.username + "/api/" + a.verb + url := "http://localhost/apps/sciencemesh/~" + a.username + "/api/auth/" + a.verb log.Info().Msgf("am.do %s %s", url, a.argS) req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) if err != nil { @@ -117,18 +123,30 @@ func (am *mgr) do(ctx context.Context, a Action) (int, []byte, error) { // Authenticate method as defined in https://github.com/cs3org/reva/blob/28500a8/pkg/auth/auth.go#L31-L33 func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) { - var params = map[string]string{ - "password": clientSecret, - // "username": clientID, + type paramsObj struct { + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + // Scope authpb.Scope } - bodyStr, err := json.Marshal(params) + bodyObj := ¶msObj{ + ClientID: clientID, + ClientSecret: clientSecret, + // Scope: authpb.Scope{ + // Resource: &types.OpaqueEntry{ + // Decoder: "json", + // Value: []byte(`{"resource_id":{"storage_id":"storage-id","opaque_id":"opaque-id"},"path":"some/file/path.txt"}`), + // }, + // Role: authpb.Role_ROLE_OWNER, + // }, + } + bodyStr, err := json.Marshal(bodyObj) if err != nil { return nil, nil, err } log := appctx.GetLogger(ctx) log.Info().Msgf("Authenticate %s %s", clientID, bodyStr) - statusCode, _, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)}) + statusCode, body, err := am.do(ctx, Action{"Authenticate", clientID, string(bodyStr)}) if err != nil { return nil, nil, err @@ -137,16 +155,20 @@ func (am *mgr) Authenticate(ctx context.Context, clientID, clientSecret string) if statusCode != 200 { return nil, nil, errors.New("Username/password not recognized by Nextcloud backend") } - user := &user.User{ - Username: clientID, - Id: &user.UserId{ - OpaqueId: clientID, - Idp: "localhost", - Type: 1, - }, - Mail: clientID, - DisplayName: clientID, - Groups: nil, + + type resultsObj struct { + User user.User `json:"user"` + Scopes map[string]authpb.Scope `json:"scopes"` + } + result := &resultsObj{} + err = json.Unmarshal(body, &result) + if err != nil { + return nil, nil, err + } + var pointersMap = make(map[string]*authpb.Scope) + for k := range result.Scopes { + scope := result.Scopes[k] + pointersMap[k] = &scope } - return user, nil, nil + return &result.User, pointersMap, nil } diff --git a/pkg/auth/manager/nextcloud/nextcloud_server_mock.go b/pkg/auth/manager/nextcloud/nextcloud_server_mock.go new file mode 100644 index 0000000000..67914a0887 --- /dev/null +++ b/pkg/auth/manager/nextcloud/nextcloud_server_mock.go @@ -0,0 +1,107 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" +) + +// Response contains data for the Nextcloud mock server to respond +// and to switch to a new server state +type Response struct { + code int + body string + newServerState string +} + +const serverStateError = "ERROR" +const serverStateEmpty = "EMPTY" +const serverStateHome = "HOME" + +var serverState = serverStateEmpty + +var responses = map[string]Response{ + `POST /apps/sciencemesh/~einstein/api/auth/Authenticate {"clientID":"einstein","clientSecret":"relativity"}`: {200, `{"user":{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}},"scopes":{"user":{"resource":{"decoder":"json","value":"eyJyZXNvdXJjZV9pZCI6eyJzdG9yYWdlX2lkIjoic3RvcmFnZS1pZCIsIm9wYXF1ZV9pZCI6Im9wYXF1ZS1pZCJ9LCJwYXRoIjoic29tZS9maWxlL3BhdGgudHh0In0="},"role":1}}}`, serverStateHome}, +} + +// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server +func GetNextcloudServerMock(called *[]string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + _, err := io.Copy(buf, r.Body) + if err != nil { + panic("Error reading response into buffer") + } + var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String()) + fmt.Printf("Nextcloud Server Mock key components %s %d %s %d %s %d\n", r.Method, len(r.Method), r.URL.String(), len(r.URL.String()), buf.String(), len(buf.String())) + fmt.Printf("Nextcloud Server Mock key %s\n", key) + *called = append(*called, key) + response := responses[key] + if (response == Response{}) { + key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState) + fmt.Printf("Nextcloud Server Mock key with State %s\n", key) + // *called = append(*called, key) + response = responses[key] + } + if (response == Response{}) { + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + fmt.Printf("Nextcloud Server Mock key not found! %s\n", key) + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + response = Response{200, fmt.Sprintf("response not defined! %s", key), serverStateEmpty} + } + serverState = responses[key].newServerState + if serverState == `` { + serverState = serverStateError + } + w.WriteHeader(response.code) + // w.Header().Set("Etag", "mocker-etag") + _, err = w.Write([]byte(responses[key].body)) + if err != nil { + panic(err) + } + }) +} + +// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2 +// Ideally, this function would live in tests/helpers, but +// if we put it there, it gets excluded by .dockerignore, and the +// Docker build fails (see https://github.com/cs3org/reva/issues/1999) +// So putting it here for now - open to suggestions if someone knows +// a better way to inject this. +func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { + s := httptest.NewServer(handler) + + cli := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, s.Listener.Addr().String()) + }, + }, + } + + return cli, s.Close +} diff --git a/pkg/auth/manager/nextcloud/nextcloud_suite_test.go b/pkg/auth/manager/nextcloud/nextcloud_suite_test.go new file mode 100644 index 0000000000..7d75b64879 --- /dev/null +++ b/pkg/auth/manager/nextcloud/nextcloud_suite_test.go @@ -0,0 +1,31 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestNextcloud(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nextcloud Suite") +} diff --git a/pkg/auth/manager/nextcloud/nextcloud_test.go b/pkg/auth/manager/nextcloud/nextcloud_test.go new file mode 100644 index 0000000000..b774b6e7ca --- /dev/null +++ b/pkg/auth/manager/nextcloud/nextcloud_test.go @@ -0,0 +1,136 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud_test + +import ( + "context" + "os" + + "google.golang.org/grpc/metadata" + + authpb "github.com/cs3org/go-cs3apis/cs3/auth/provider/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + + "github.com/cs3org/reva/pkg/auth/manager/nextcloud" + "github.com/cs3org/reva/pkg/auth/scope" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + jwt "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/tests/helpers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Nextcloud", func() { + var ( + ctx context.Context + options map[string]interface{} + tmpRoot string + user = &userpb.User{ + Id: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "tester", + } + ) + + BeforeEach(func() { + var err error + tmpRoot, err := helpers.TempDir("reva-unit-tests-*-root") + Expect(err).ToNot(HaveOccurred()) + + options = map[string]interface{}{ + "root": tmpRoot, + "enable_home": true, + "share_folder": "/Shares", + } + + ctx = context.Background() + + // Add auth token + tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"}) + Expect(err).ToNot(HaveOccurred()) + scope, err := scope.AddOwnerScope(nil) + Expect(err).ToNot(HaveOccurred()) + t, err := tokenManager.MintToken(ctx, user, scope) + Expect(err).ToNot(HaveOccurred()) + ctx = ctxpkg.ContextSetToken(ctx, t) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, t) + ctx = ctxpkg.ContextSetUser(ctx, user) + }) + + AfterEach(func() { + if tmpRoot != "" { + os.RemoveAll(tmpRoot) + } + }) + + Describe("New", func() { + It("returns a new instance", func() { + _, err := nextcloud.New(options) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + // Authenticate(ctx context.Context, clientID, clientSecret string) (*user.User, map[string]*authpb.Scope, error) + Describe("Authenticate", func() { + It("calls the GetHome endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + am, _ := nextcloud.NewAuthManager(&nextcloud.AuthManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + user, scope, err := am.Authenticate(ctx, "einstein", "relativity") + Expect(err).ToNot(HaveOccurred()) + Expect(user).To(Equal(&userpb.User{ + Id: &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + UidNumber: 0, + GidNumber: 0, + })) + Expect(scope).To(Equal(map[string]*authpb.Scope{ + "user": { + Resource: &types.OpaqueEntry{ + Decoder: "json", + Value: []byte("{\"resource_id\":{\"storage_id\":\"storage-id\",\"opaque_id\":\"opaque-id\"},\"path\":\"some/file/path.txt\"}"), + }, + Role: 1, + }, + })) + Expect(len(called)).To(Equal(1)) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~einstein/api/auth/Authenticate {"clientID":"einstein","clientSecret":"relativity"}`)) + }) + }) + +}) diff --git a/pkg/user/manager/nextcloud/nextcloud.go b/pkg/user/manager/nextcloud/nextcloud.go index 08219fdbda..db002871d1 100644 --- a/pkg/user/manager/nextcloud/nextcloud.go +++ b/pkg/user/manager/nextcloud/nextcloud.go @@ -20,10 +20,14 @@ package nextcloud import ( "context" + "encoding/json" "io" "net/http" "strings" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + + "github.com/cs3org/reva/pkg/errtypes" "github.com/cs3org/reva/pkg/user" "github.com/cs3org/reva/pkg/user/manager/registry" "github.com/mitchellh/mapstructure" @@ -42,19 +46,19 @@ type manager struct { endPoint string } -type config struct { - EndPoint string `mapstructure:"end_point"` - MockHTTP bool `mapstructure:"mock_http"` +// UserManagerConfig contains config for a Nextcloud-based UserManager +type UserManagerConfig struct { + EndPoint string `mapstructure:"endpoint" docs:";The Nextcloud backend endpoint for user management"` } -func (c *config) init() { +func (c *UserManagerConfig) init() { if c.EndPoint == "" { c.EndPoint = "http://localhost/end/point?" } } -func parseConfig(m map[string]interface{}) (*config, error) { - c := &config{} +func parseConfig(m map[string]interface{}) (*UserManagerConfig, error) { + c := &UserManagerConfig{} if err := mapstructure.Decode(m, c); err != nil { err = errors.Wrap(err, "error decoding conf") return nil, err @@ -76,14 +80,32 @@ func New(m map[string]interface{}) (user.Manager, error) { return nil, err } + return NewUserManager(c, &http.Client{}) +} + +// NewUserManager returns a new Nextcloud-based UserManager +func NewUserManager(c *UserManagerConfig, hc *http.Client) (user.Manager, error) { return &manager{ - client: &http.Client{}, endPoint: c.EndPoint, // e.g. "http://nc/apps/sciencemesh/" + client: hc, }, nil } -func (m *manager) do(a Action) (int, []byte, error) { - url := m.endPoint + a.verb +func getUser(ctx context.Context) (*userpb.User, error) { + u, ok := ctxpkg.ContextGetUser(ctx) + if !ok { + err := errors.Wrap(errtypes.UserRequired(""), "nextcloud storage driver: error getting user from ctx") + return nil, err + } + return u, nil +} + +func (m *manager) do(ctx context.Context, a Action) (int, []byte, error) { + user, err := getUser(ctx) + if err != nil { + return 0, nil, err + } + url := m.endPoint + "~" + user.Username + "/api/user/" + a.verb req, err := http.NewRequest(http.MethodPost, url, strings.NewReader(a.argS)) if err != nil { panic(err) @@ -105,51 +127,75 @@ func (m *manager) Configure(ml map[string]interface{}) error { } func (m *manager) GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User, error) { - _, respBody, err := m.do(Action{"GetUser", uid.Idp}) - u := &userpb.User{ - Username: string(respBody), + bodyStr, err := json.Marshal(uid) + if err != nil { + return nil, err + } + _, respBody, err := m.do(ctx, Action{"GetUser", string(bodyStr)}) + if err != nil { + return nil, err } - return u, err -} -func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, error) { - _, respBody, err := m.do(Action{"FindUsers", query}) - u := &userpb.User{ - Username: string(respBody), + result := &userpb.User{} + err = json.Unmarshal(respBody, &result) + if err != nil { + return nil, err } - var us = make([]*userpb.User, 1) - us[0] = u - return us, err + return result, err } func (m *manager) GetUserByClaim(ctx context.Context, claim, value string) (*userpb.User, error) { - _, respBody, err := m.do(Action{"GetUserByClaim", value}) - u := &userpb.User{ - Username: string(respBody), + type paramsObj struct { + Claim string `json:"claim"` + Value string `json:"value"` } - return u, err + bodyObj := ¶msObj{ + Claim: claim, + Value: value, + } + bodyStr, _ := json.Marshal(bodyObj) + _, respBody, err := m.do(ctx, Action{"GetUserByClaim", string(bodyStr)}) + if err != nil { + return nil, err + } + result := &userpb.User{} + err = json.Unmarshal(respBody, &result) + if err != nil { + return nil, err + } + return result, err } -// func extractClaim(u *userpb.User, claim string) (string, error) { -// _, respBody, err := m.do(Action{"ExtractClaim", claim}) -// u := &userpb.User{ -// Username: string(respBody), -// } -// return u, err -// } - -// func userContains(u *userpb.User, query string) bool { -// _, respBody, err := m.do(Action{"userContains", query}) -// u := &userpb.User{ -// Username: string(respBody), -// } -// return u, err -// return false -// } - func (m *manager) GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) { - _, respBody, err := m.do(Action{"GetUserGroups", uid.Idp}) - var gs = make([]string, 1) - gs[0] = string(respBody) + bodyStr, err := json.Marshal(uid) + if err != nil { + return nil, err + } + _, respBody, err := m.do(ctx, Action{"GetUserGroups", string(bodyStr)}) + if err != nil { + return nil, err + } + var gs []string + err = json.Unmarshal(respBody, &gs) + if err != nil { + return nil, err + } return gs, err } + +func (m *manager) FindUsers(ctx context.Context, query string) ([]*userpb.User, error) { + _, respBody, err := m.do(ctx, Action{"FindUsers", query}) + if err != nil { + return nil, err + } + var respArr []userpb.User + err = json.Unmarshal(respBody, &respArr) + if err != nil { + return nil, err + } + var pointers = make([]*userpb.User, len(respArr)) + for i := 0; i < len(respArr); i++ { + pointers[i] = &respArr[i] + } + return pointers, err +} diff --git a/pkg/user/manager/nextcloud/nextcloud_server_mock.go b/pkg/user/manager/nextcloud/nextcloud_server_mock.go new file mode 100644 index 0000000000..213f9eae51 --- /dev/null +++ b/pkg/user/manager/nextcloud/nextcloud_server_mock.go @@ -0,0 +1,110 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud + +import ( + "context" + "fmt" + "io" + "net" + "net/http" + "net/http/httptest" + "strings" +) + +// Response contains data for the Nextcloud mock server to respond +// and to switch to a new server state +type Response struct { + code int + body string + newServerState string +} + +const serverStateError = "ERROR" +const serverStateEmpty = "EMPTY" +const serverStateHome = "HOME" + +var serverState = serverStateEmpty + +var responses = map[string]Response{ + `POST /apps/sciencemesh/~tester/api/user/GetUser {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/user/GetUserByClaim {"claim":"claim-string","value":"value-string"}`: {200, `{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/user/GetUserGroups {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`: {200, `["wine-lovers"]`, serverStateHome}, + `POST /apps/sciencemesh/~tester/api/user/FindUsers some-query`: {200, `[{"id":{"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}}]`, serverStateHome}, +} + +// GetNextcloudServerMock returns a handler that pretends to be a remote Nextcloud server +func GetNextcloudServerMock(called *[]string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(strings.Builder) + _, err := io.Copy(buf, r.Body) + if err != nil { + panic("Error reading response into buffer") + } + var key = fmt.Sprintf("%s %s %s", r.Method, r.URL, buf.String()) + fmt.Printf("Nextcloud Server Mock key components %s %d %s %d %s %d\n", r.Method, len(r.Method), r.URL.String(), len(r.URL.String()), buf.String(), len(buf.String())) + fmt.Printf("Nextcloud Server Mock key %s\n", key) + *called = append(*called, key) + response := responses[key] + if (response == Response{}) { + key = fmt.Sprintf("%s %s %s %s", r.Method, r.URL, buf.String(), serverState) + fmt.Printf("Nextcloud Server Mock key with State %s\n", key) + // *called = append(*called, key) + response = responses[key] + } + if (response == Response{}) { + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + fmt.Printf("Nextcloud Server Mock key not found! %s\n", key) + fmt.Println("ERROR!!") + fmt.Println("ERROR!!") + response = Response{200, fmt.Sprintf("response not defined! %s", key), serverStateEmpty} + } + serverState = responses[key].newServerState + if serverState == `` { + serverState = serverStateError + } + w.WriteHeader(response.code) + // w.Header().Set("Etag", "mocker-etag") + _, err = w.Write([]byte(responses[key].body)) + if err != nil { + panic(err) + } + }) +} + +// TestingHTTPClient thanks to https://itnext.io/how-to-stub-requests-to-remote-hosts-with-go-6c2c1db32bf2 +// Ideally, this function would live in tests/helpers, but +// if we put it there, it gets excluded by .dockerignore, and the +// Docker build fails (see https://github.com/cs3org/reva/issues/1999) +// So putting it here for now - open to suggestions if someone knows +// a better way to inject this. +func TestingHTTPClient(handler http.Handler) (*http.Client, func()) { + s := httptest.NewServer(handler) + + cli := &http.Client{ + Transport: &http.Transport{ + DialContext: func(_ context.Context, network, _ string) (net.Conn, error) { + return net.Dial(network, s.Listener.Addr().String()) + }, + }, + } + + return cli, s.Close +} diff --git a/pkg/user/manager/nextcloud/nextcloud_suite_test.go b/pkg/user/manager/nextcloud/nextcloud_suite_test.go new file mode 100644 index 0000000000..7d75b64879 --- /dev/null +++ b/pkg/user/manager/nextcloud/nextcloud_suite_test.go @@ -0,0 +1,31 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestNextcloud(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Nextcloud Suite") +} diff --git a/pkg/user/manager/nextcloud/nextcloud_test.go b/pkg/user/manager/nextcloud/nextcloud_test.go new file mode 100644 index 0000000000..a150f5e731 --- /dev/null +++ b/pkg/user/manager/nextcloud/nextcloud_test.go @@ -0,0 +1,214 @@ +// Copyright 2018-2021 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package nextcloud_test + +import ( + "context" + "os" + + "google.golang.org/grpc/metadata" + + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + + "github.com/cs3org/reva/pkg/auth/scope" + ctxpkg "github.com/cs3org/reva/pkg/ctx" + jwt "github.com/cs3org/reva/pkg/token/manager/jwt" + "github.com/cs3org/reva/pkg/user/manager/nextcloud" + "github.com/cs3org/reva/tests/helpers" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Nextcloud", func() { + var ( + ctx context.Context + options map[string]interface{} + tmpRoot string + user = &userpb.User{ + Id: &userpb.UserId{ + Idp: "0.0.0.0:19000", + OpaqueId: "f7fbf8c8-139b-4376-b307-cf0a8c2d0d9c", + Type: userpb.UserType_USER_TYPE_PRIMARY, + }, + Username: "tester", + } + ) + + BeforeEach(func() { + var err error + tmpRoot, err := helpers.TempDir("reva-unit-tests-*-root") + Expect(err).ToNot(HaveOccurred()) + + options = map[string]interface{}{ + "root": tmpRoot, + "enable_home": true, + "share_folder": "/Shares", + } + + ctx = context.Background() + + // Add auth token + tokenManager, err := jwt.New(map[string]interface{}{"secret": "changemeplease"}) + Expect(err).ToNot(HaveOccurred()) + scope, err := scope.AddOwnerScope(nil) + Expect(err).ToNot(HaveOccurred()) + t, err := tokenManager.MintToken(ctx, user, scope) + Expect(err).ToNot(HaveOccurred()) + ctx = ctxpkg.ContextSetToken(ctx, t) + ctx = metadata.AppendToOutgoingContext(ctx, ctxpkg.TokenHeader, t) + ctx = ctxpkg.ContextSetUser(ctx, user) + }) + + AfterEach(func() { + if tmpRoot != "" { + os.RemoveAll(tmpRoot) + } + }) + + Describe("New", func() { + It("returns a new instance", func() { + _, err := nextcloud.New(options) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + // GetUser(ctx context.Context, uid *userpb.UserId) (*userpb.User, error) + Describe("GetUser", func() { + It("calls the GetUser endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + um, _ := nextcloud.NewUserManager(&nextcloud.UserManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + user, err := um.GetUser(ctx, &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(user).To(Equal(&userpb.User{ + Id: &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + UidNumber: 0, + GidNumber: 0, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/user/GetUser {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`)) + }) + }) + + // GetUserByClaim(ctx context.Context, claim, value string) (*userpb.User, error) + Describe("GetUserByClaim", func() { + It("calls the GetUserByClaim endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + um, _ := nextcloud.NewUserManager(&nextcloud.UserManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + user, err := um.GetUserByClaim(ctx, "claim-string", "value-string") + Expect(err).ToNot(HaveOccurred()) + Expect(user).To(Equal(&userpb.User{ + Id: &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + UidNumber: 0, + GidNumber: 0, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/user/GetUserByClaim {"claim":"claim-string","value":"value-string"}`)) + }) + }) + + // GetUserGroups(ctx context.Context, uid *userpb.UserId) ([]string, error) + Describe("GetUserGroups", func() { + It("calls the GetUserGroups endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + um, _ := nextcloud.NewUserManager(&nextcloud.UserManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + groups, err := um.GetUserGroups(ctx, &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(groups).To(Equal([]string{"wine-lovers"})) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/user/GetUserGroups {"idp":"some-idp","opaque_id":"some-opaque-user-id","type":1}`)) + }) + }) + + // FindUsers(ctx context.Context, query string) ([]*userpb.User, error) + Describe("FindUsers", func() { + It("calls the FindUsers endpoint", func() { + called := make([]string, 0) + + h := nextcloud.GetNextcloudServerMock(&called) + mock, teardown := nextcloud.TestingHTTPClient(h) + defer teardown() + um, _ := nextcloud.NewUserManager(&nextcloud.UserManagerConfig{ + EndPoint: "http://mock.com/apps/sciencemesh/", + }, mock) + users, err := um.FindUsers(ctx, "some-query") + Expect(err).ToNot(HaveOccurred()) + Expect(len(users)).To(Equal(1)) + Expect(*users[0]).To(Equal(userpb.User{ + Id: &userpb.UserId{ + Idp: "some-idp", + OpaqueId: "some-opaque-user-id", + Type: 1, + }, + Username: "", + Mail: "", + MailVerified: false, + DisplayName: "", + Groups: nil, + Opaque: nil, + UidNumber: 0, + GidNumber: 0, + })) + Expect(called[0]).To(Equal(`POST /apps/sciencemesh/~tester/api/user/FindUsers some-query`)) + }) + }) +})