From 6880b6cd707954b842248cb3d1ba29fbbea93d29 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 18 Jan 2024 16:57:56 +0000 Subject: [PATCH 01/26] Add get all user channels endpoint and TestGetAllChannelsByUser --- rest/diagnostic_api.go | 27 +++++++++++++ rest/diagnostic_api_test.go | 80 +++++++++++++++++++++++++++++++++++++ rest/routing.go | 3 ++ 3 files changed, 110 insertions(+) create mode 100644 rest/diagnostic_api.go create mode 100644 rest/diagnostic_api_test.go diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go new file mode 100644 index 0000000000..e42171668a --- /dev/null +++ b/rest/diagnostic_api.go @@ -0,0 +1,27 @@ +package rest + +import ( + "github.com/couchbase/sync_gateway/base" + "github.com/gorilla/mux" +) + +type allChannels struct { + Channels base.Set `json:"all_channels,omitempty"` +} + +func (h *handler) handleGetAllChannels() error { + h.assertAdminOnly() + user, err := h.db.Authenticator(h.ctx()).GetUser(internalUserName(mux.Vars(h.rq)["name"])) + if user == nil { + if err == nil { + err = kNotFoundError + } + return err + } + info := marshalPrincipal(h.db, user, true) + + channels := allChannels{Channels: info.Channels} + bytes, err := base.JSONMarshal(channels) + h.writeRawJSON(bytes) + return err +} diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go new file mode 100644 index 0000000000..8af22b1a1e --- /dev/null +++ b/rest/diagnostic_api_test.go @@ -0,0 +1,80 @@ +package rest + +import ( + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "net/http" + "testing" +) + +func TestGetAllChannelsByUser(t *testing.T) { + rt := NewRestTester(t, &RestTesterConfig{ + PersistentConfig: true, + SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel);}`, + }) + defer rt.Close() + + dbConfig := rt.NewDbConfig() + + dbName := "db" + rt.CreateDatabase("db", dbConfig) + + // create sessions before users + const alice = "alice" + const bob = "bob" + + // Put user alice and assert admin assigned channels are returned by the get all_channels endpoint + response := rt.SendAdminRequest(http.MethodPut, + "/"+dbName+"/_user/"+alice, + `{"name": "`+alice+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": ["A","B","C"]}`) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+alice+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + + var channelMap allChannels + err := json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!"}) + + // Assert non existent user returns 404 + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusNotFound) + + // Put user bob and assert on channels returned by all_channels + response = rt.SendAdminRequest(http.MethodPut, + "/"+dbName+"/_user/"+bob, + `{"name": "`+bob+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": []}`) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!"}) + + // Assign new channel to user bob and assert all_channels includes it + response = rt.SendAdminRequest(http.MethodPut, + "/"+dbName+"/doc1", + `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!", "NewChannel"}) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+alice+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) +} diff --git a/rest/routing.go b/rest/routing.go index eab5a66314..5d3dbb5848 100644 --- a/rest/routing.go +++ b/rest/routing.go @@ -183,6 +183,9 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router { dbr.Handle("/_user/{name}", makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUser)).Methods("DELETE") + dbr.Handle("/_user/{name}/all_channels", + makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).handleGetAllChannels)).Methods("GET", "HEAD") + dbr.Handle("/_user/{name}/_session", makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUserSessions)).Methods("DELETE") dbr.Handle("/_user/{name}/_session/{sessionid}", From 4136a2001bb2505e3061b99877b7bd3870916c75 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 18 Jan 2024 17:04:45 +0000 Subject: [PATCH 02/26] Fix lint --- rest/diagnostic_api_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 8af22b1a1e..ba34c79078 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -2,10 +2,11 @@ package rest import ( "encoding/json" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "net/http" "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetAllChannelsByUser(t *testing.T) { From 86454fff9e04377230cb65679664c535e66a2a19 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 18 Jan 2024 17:07:23 +0000 Subject: [PATCH 03/26] Add licensing comments --- rest/diagnostic_api.go | 8 ++++++++ rest/diagnostic_api_test.go | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index e42171668a..6f7dd905d5 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -1,3 +1,11 @@ +// Copyright 2013-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. + package rest import ( diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index ba34c79078..9e681209ee 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -1,3 +1,11 @@ +// Copyright 2013-Present Couchbase, Inc. +// +// Use of this software is governed by the Business Source License included +// in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +// in that file, in accordance with the Business Source License, use of this +// software will be governed by the Apache License, Version 2.0, included in +// the file licenses/APL2.txt. + package rest import ( From cc0e033e2f30d8ed2f440023c76603ebecd31f34 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 18 Jan 2024 17:32:21 +0000 Subject: [PATCH 04/26] Fix test --- rest/diagnostic_api_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 9e681209ee..7f0ce0ae70 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -26,7 +26,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dbConfig := rt.NewDbConfig() - dbName := "db" + dbName := "{{.keyspace}}" rt.CreateDatabase("db", dbConfig) // create sessions before users From 6ffe41027e5e61044299838f7966cf6856777fdc Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 18 Jan 2024 17:59:52 +0000 Subject: [PATCH 05/26] Fix test 2 --- rest/diagnostic_api_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 7f0ce0ae70..9e681209ee 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -26,7 +26,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dbConfig := rt.NewDbConfig() - dbName := "{{.keyspace}}" + dbName := "db" rt.CreateDatabase("db", dbConfig) // create sessions before users From cb728fabf603a740110e6a093aa657af2efbd1e6 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Fri, 19 Jan 2024 11:04:28 +0000 Subject: [PATCH 06/26] Add walrus check --- rest/diagnostic_api_test.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 9e681209ee..8e53b37f3b 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -10,14 +10,18 @@ package rest import ( "encoding/json" + "github.com/couchbase/sync_gateway/base" + "github.com/stretchr/testify/assert" "net/http" "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestGetAllChannelsByUser(t *testing.T) { + if base.UnitTestUrlIsWalrus() { + t.Skip("Test requires Couchbase Server") + } rt := NewRestTester(t, &RestTesterConfig{ PersistentConfig: true, SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel);}`, @@ -69,7 +73,7 @@ func TestGetAllChannelsByUser(t *testing.T) { // Assign new channel to user bob and assert all_channels includes it response = rt.SendAdminRequest(http.MethodPut, - "/"+dbName+"/doc1", + "/{{.keyspace}}/doc1", `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) RequireStatus(t, response, http.StatusCreated) From 00a38f7fd9dd6ebb3ed34b26c0230094dc926a3f Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Fri, 19 Jan 2024 11:07:11 +0000 Subject: [PATCH 07/26] Fix goimports --- rest/diagnostic_api_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 8e53b37f3b..6938570d2e 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -10,11 +10,12 @@ package rest import ( "encoding/json" - "github.com/couchbase/sync_gateway/base" - "github.com/stretchr/testify/assert" "net/http" "testing" + "github.com/couchbase/sync_gateway/base" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) From 09351969d4d8883a648bd764b2098305d8179704 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 14:01:11 +0000 Subject: [PATCH 08/26] Add collections test and behaviour to endpoint --- rest/diagnostic_api.go | 13 ++-- rest/diagnostic_api_test.go | 132 ++++++++++++++++++++++++++++++++++-- 2 files changed, 137 insertions(+), 8 deletions(-) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 6f7dd905d5..24305eea23 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -20,15 +20,20 @@ type allChannels struct { func (h *handler) handleGetAllChannels() error { h.assertAdminOnly() user, err := h.db.Authenticator(h.ctx()).GetUser(internalUserName(mux.Vars(h.rq)["name"])) - if user == nil { - if err == nil { - err = kNotFoundError - } + if err != nil { return err } + if user == nil { + return kNotFoundError + } info := marshalPrincipal(h.db, user, true) channels := allChannels{Channels: info.Channels} + if !h.db.OnlyDefaultCollection() { + bytes, err := base.JSONMarshal(info.CollectionAccess) + h.writeRawJSON(bytes) + return err + } bytes, err := base.JSONMarshal(channels) h.writeRawJSON(bytes) return err diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 6938570d2e..5e5c15cc4d 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -10,9 +10,12 @@ package rest import ( "encoding/json" + "fmt" "net/http" "testing" + "github.com/couchbase/sync_gateway/auth" + "github.com/couchbase/sync_gateway/base" "github.com/stretchr/testify/assert" @@ -25,7 +28,7 @@ func TestGetAllChannelsByUser(t *testing.T) { } rt := NewRestTester(t, &RestTesterConfig{ PersistentConfig: true, - SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel);}`, + SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel); role(doc.user, doc.role);}`, }) defer rt.Close() @@ -49,7 +52,7 @@ func TestGetAllChannelsByUser(t *testing.T) { RequireStatus(t, response, http.StatusOK) var channelMap allChannels - err := json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err := json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!"}) @@ -68,7 +71,7 @@ func TestGetAllChannelsByUser(t *testing.T) { "/"+dbName+"/_user/"+bob+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!"}) @@ -81,7 +84,7 @@ func TestGetAllChannelsByUser(t *testing.T) { response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+bob+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!", "NewChannel"}) @@ -91,4 +94,125 @@ func TestGetAllChannelsByUser(t *testing.T) { err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) + + response = rt.SendAdminRequest("PUT", "/db/_role/role1", `{"admin_channels":["chan"]}`) + RequireStatus(t, response, http.StatusCreated) + + // Assign new channel to user bob and assert all_channels includes it + response = rt.SendAdminRequest(http.MethodPut, + "/{{.keyspace}}/doc2", + `{"role":"role:role1", "user":"bob"}`) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + err = json.Unmarshal(response.BodyBytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!", "NewChannel", "chan"}) + +} + +func TestGetAllChannelsByUserWithCollections(t *testing.T) { + SyncFn := `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel);}` + + rt := NewRestTester(t, &RestTesterConfig{ + PersistentConfig: true, + }) + defer rt.Close() + + dbName := "db" + tb := base.GetTestBucket(t) + defer tb.Close(rt.Context()) + scopesConfig := GetCollectionsConfig(t, tb, 2) + for scope, _ := range scopesConfig { + for _, cnf := range scopesConfig[scope].Collections { + cnf.SyncFn = &SyncFn + } + } + dbConfig := makeDbConfig(tb.GetName(), dbName, scopesConfig) + rt.CreateDatabase("db", dbConfig) + + scopeName := rt.GetDbCollections()[0].ScopeName + collection1Name := rt.GetDbCollections()[0].Name + collection2Name := rt.GetDbCollections()[1].Name + scopesConfig[scopeName].Collections[collection1Name] = &CollectionConfig{} + + collectionPayload := fmt.Sprintf(`,"%s": { + "admin_channels":["a"] + }`, collection2Name) + + // create sessions before users + const alice = "alice" + const bob = "bob" + userPayload := `{ + %s + "admin_channels":["foo", "bar"], + "collection_access": { + "%s": { + "%s": { + "admin_channels":["A", "B", "C"] + } + %s + } + } + }` + // Put user alice and assert admin assigned channels are returned by the get all_channels endpoint + response := rt.SendAdminRequest(http.MethodPut, + "/"+dbName+"/_user/"+alice, fmt.Sprintf(userPayload, `"email":"bob@couchbase.com","password":"letmein",`, + scopeName, collection1Name, collectionPayload)) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+alice+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + t.Log(response.Body.String()) + var channelMap map[string]map[string]*auth.CollectionAccessConfig + err := json.Unmarshal(response.BodyBytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"A", "B", "C", "!"}) + + // Assert non existent user returns 404 + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusNotFound) + + // Put user bob and assert on channels returned by all_channels + response = rt.SendAdminRequest(http.MethodPut, + "/"+dbName+"/_user/"+bob, + `{"name": "`+bob+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": []}`) + RequireStatus(t, response, http.StatusCreated) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!"}) + + // Assign new channel to user bob and assert all_channels includes it + response = rt.SendAdminRequest(http.MethodPut, + fmt.Sprintf("/%s/%s", rt.GetKeyspaces()[0], "doc1"), + `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) + RequireStatus(t, response, http.StatusCreated) + t.Log(rt.GetKeyspaces()[1]) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+bob+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + t.Log(response.Body.String()) + + require.NoError(t, err) + assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!", "NewChannel"}) + + response = rt.SendAdminRequest(http.MethodGet, + "/"+dbName+"/_user/"+alice+"/all_channels", ``) + RequireStatus(t, response, http.StatusOK) + t.Log(response.Body.String()) + + err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + require.NoError(t, err) + assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) } From b66431b29bba08f691807a96ad139e8b0c4b1cd2 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 14:16:23 +0000 Subject: [PATCH 09/26] Use bodybytes instead of ResponseRecorder.Body.Bytes --- rest/diagnostic_api_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 5e5c15cc4d..c74b83a9b8 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -91,7 +91,7 @@ func TestGetAllChannelsByUser(t *testing.T) { response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+alice+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) @@ -187,7 +187,7 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { "/"+dbName+"/_user/"+bob+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!"}) @@ -201,7 +201,7 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+bob+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) t.Log(response.Body.String()) require.NoError(t, err) @@ -212,7 +212,7 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { RequireStatus(t, response, http.StatusOK) t.Log(response.Body.String()) - err = json.Unmarshal(response.ResponseRecorder.Body.Bytes(), &channelMap) + err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) } From f83cc24f8cf685dcc525848d3bcc51f61fd11e13 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 15:59:13 +0000 Subject: [PATCH 10/26] Return map[string]map[string]allChannels for collections --- rest/diagnostic_api.go | 7 +++++++ rest/diagnostic_api_test.go | 8 +------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 24305eea23..5009bded5f 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -30,6 +30,13 @@ func (h *handler) handleGetAllChannels() error { channels := allChannels{Channels: info.Channels} if !h.db.OnlyDefaultCollection() { + allCollectionChannels := make(map[string]map[string]allChannels) + for scope, collectionConfig := range info.CollectionAccess { + for collectionName, CAConfig := range collectionConfig { + allCollectionChannels[scope] = make(map[string]allChannels) + allCollectionChannels[scope][collectionName] = allChannels{Channels: CAConfig.Channels_} + } + } bytes, err := base.JSONMarshal(info.CollectionAccess) h.writeRawJSON(bytes) return err diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index c74b83a9b8..fb569db314 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -23,9 +23,6 @@ import ( ) func TestGetAllChannelsByUser(t *testing.T) { - if base.UnitTestUrlIsWalrus() { - t.Skip("Test requires Couchbase Server") - } rt := NewRestTester(t, &RestTesterConfig{ PersistentConfig: true, SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel); role(doc.user, doc.role);}`, @@ -166,7 +163,7 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+alice+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - t.Log(response.Body.String()) + var channelMap map[string]map[string]*auth.CollectionAccessConfig err := json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) @@ -196,13 +193,11 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { fmt.Sprintf("/%s/%s", rt.GetKeyspaces()[0], "doc1"), `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) RequireStatus(t, response, http.StatusCreated) - t.Log(rt.GetKeyspaces()[1]) response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+bob+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) err = json.Unmarshal(response.BodyBytes(), &channelMap) - t.Log(response.Body.String()) require.NoError(t, err) assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!", "NewChannel"}) @@ -210,7 +205,6 @@ func TestGetAllChannelsByUserWithCollections(t *testing.T) { response = rt.SendAdminRequest(http.MethodGet, "/"+dbName+"/_user/"+alice+"/all_channels", ``) RequireStatus(t, response, http.StatusOK) - t.Log(response.Body.String()) err = json.Unmarshal(response.BodyBytes(), &channelMap) require.NoError(t, err) From 4889f5cbb3c8dd5963a91e9fa89a1c619687ce63 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 16:28:21 +0000 Subject: [PATCH 11/26] Redisable TestGetAllChannelsByUser with walrus --- rest/diagnostic_api_test.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index fb569db314..d1ab454875 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -23,9 +23,14 @@ import ( ) func TestGetAllChannelsByUser(t *testing.T) { + if base.TestsUseNamedCollections() { + t.Skip("Test requires Couchbase Server") + } + rt := NewRestTester(t, &RestTesterConfig{ PersistentConfig: true, - SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel); role(doc.user, doc.role);}`, + + SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel); role(doc.user, doc.role);}`, }) defer rt.Close() From cefb1b4f0525484f6c546c89b4f4e40ac8fbd85a Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 17:46:33 +0000 Subject: [PATCH 12/26] Add docs --- docs/api/admin.yaml | 2 ++ .../admin/db-_user-name-_all_channels.yaml | 28 +++++++++++++++++++ rest/routing.go | 2 +- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 docs/api/paths/admin/db-_user-name-_all_channels.yaml diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml index 145b752805..2f2b4b566b 100644 --- a/docs/api/admin.yaml +++ b/docs/api/admin.yaml @@ -40,6 +40,8 @@ paths: $ref: './paths/admin/db-_user-.yaml' '/{db}/_user/{name}': $ref: './paths/admin/db-_user-name.yaml' + '/{db}/_user/{name}/all_channels': + $ref: './paths/admin/db-_user-name/all_channels.yaml' '/{db}/_user/{name}/_session': $ref: './paths/admin/db-_user-name-_session.yaml' '/{db}/_user/{name}/_session/{sessionid}': diff --git a/docs/api/paths/admin/db-_user-name-_all_channels.yaml b/docs/api/paths/admin/db-_user-name-_all_channels.yaml new file mode 100644 index 0000000000..a95bdd3551 --- /dev/null +++ b/docs/api/paths/admin/db-_user-name-_all_channels.yaml @@ -0,0 +1,28 @@ +# Copyright 2022-Present Couchbase, Inc. +# +# Use of this software is governed by the Business Source License included +# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +# in that file, in accordance with the Business Source License, use of this +# software will be governed by the Apache License, Version 2.0, included in +# the file licenses/APL2.txt. +parameters: + - $ref: ../../components/parameters.yaml#/db + - $ref: ../../components/parameters.yaml#/user-name +get: + summary: Get all channels for a user + description: |- + Retrieve all channels that a user has access to. + + Required Sync Gateway RBAC roles: + + * Sync Gateway Architect + * Sync Gateway Application + * Sync Gateway Application Read Only + responses: + '200': + $ref: ../../components/responses.yaml#/User + '404': + $ref: ../../components/responses.yaml#/Not-found + tags: + - Database Security + operationId: get_db-_user-name-_all_channels diff --git a/rest/routing.go b/rest/routing.go index 5d3dbb5848..d60daee1d1 100644 --- a/rest/routing.go +++ b/rest/routing.go @@ -184,7 +184,7 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router { makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUser)).Methods("DELETE") dbr.Handle("/_user/{name}/all_channels", - makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).handleGetAllChannels)).Methods("GET", "HEAD") + makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).handleGetAllChannels)).Methods("GET") dbr.Handle("/_user/{name}/_session", makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUserSessions)).Methods("DELETE") From 6fba35889fd0ccaee81ad3445330a1ea7820add1 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Mon, 22 Jan 2024 17:50:59 +0000 Subject: [PATCH 13/26] Fix docs path --- docs/api/admin.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml index 2f2b4b566b..4f44e936d8 100644 --- a/docs/api/admin.yaml +++ b/docs/api/admin.yaml @@ -41,7 +41,7 @@ paths: '/{db}/_user/{name}': $ref: './paths/admin/db-_user-name.yaml' '/{db}/_user/{name}/all_channels': - $ref: './paths/admin/db-_user-name/all_channels.yaml' + $ref: './paths/admin/db-_user-name-_all_channels.yaml' '/{db}/_user/{name}/_session': $ref: './paths/admin/db-_user-name-_session.yaml' '/{db}/_user/{name}/_session/{sessionid}': From df40694f74056c7fd4578b23a6343b6f85bc8798 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Tue, 23 Jan 2024 10:22:14 +0000 Subject: [PATCH 14/26] Fix extra leading underscore --- docs/api/admin.yaml | 2 +- ...-name-_all_channels.yaml => db-_user-name-all_channels.yaml} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename docs/api/paths/admin/{db-_user-name-_all_channels.yaml => db-_user-name-all_channels.yaml} (95%) diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml index 4f44e936d8..d057026a6d 100644 --- a/docs/api/admin.yaml +++ b/docs/api/admin.yaml @@ -41,7 +41,7 @@ paths: '/{db}/_user/{name}': $ref: './paths/admin/db-_user-name.yaml' '/{db}/_user/{name}/all_channels': - $ref: './paths/admin/db-_user-name-_all_channels.yaml' + $ref: './paths/admin/db-_user-name-all_channels.yaml' '/{db}/_user/{name}/_session': $ref: './paths/admin/db-_user-name-_session.yaml' '/{db}/_user/{name}/_session/{sessionid}': diff --git a/docs/api/paths/admin/db-_user-name-_all_channels.yaml b/docs/api/paths/admin/db-_user-name-all_channels.yaml similarity index 95% rename from docs/api/paths/admin/db-_user-name-_all_channels.yaml rename to docs/api/paths/admin/db-_user-name-all_channels.yaml index a95bdd3551..ecb2cce6e9 100644 --- a/docs/api/paths/admin/db-_user-name-_all_channels.yaml +++ b/docs/api/paths/admin/db-_user-name-all_channels.yaml @@ -25,4 +25,4 @@ get: $ref: ../../components/responses.yaml#/Not-found tags: - Database Security - operationId: get_db-_user-name-_all_channels + operationId: get_db-_user-name-all_channels From b45020c171f4f86c69f9a8fa9229fe82f8b60aeb Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Tue, 23 Jan 2024 10:58:52 +0000 Subject: [PATCH 15/26] Make and use marshalChannels for handleGetAllChannels --- rest/admin_api.go | 48 ++++++++++++++++++++++++++++++++++++++++++ rest/diagnostic_api.go | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/rest/admin_api.go b/rest/admin_api.go index c36c22b163..48c97d21f1 100644 --- a/rest/admin_api.go +++ b/rest/admin_api.go @@ -1329,6 +1329,54 @@ func marshalPrincipal(database *db.Database, princ auth.Principal, includeDynami return info } +// marshalPrincipal outputs a PrincipalConfig in a format for REST API endpoints. +func marshalChannels(database *db.Database, princ auth.Principal, includeDynamicGrantInfo bool) auth.PrincipalConfig { + name := externalUserName(princ.Name()) + info := auth.PrincipalConfig{ + Name: &name, + ExplicitChannels: princ.ExplicitChannels().AsSet(), + } + + collectionAccess := princ.GetCollectionsAccess() + if collectionAccess != nil && !database.OnlyDefaultCollection() { + info.CollectionAccess = make(map[string]map[string]*auth.CollectionAccessConfig) + for scopeName, scope := range collectionAccess { + scopeAccessConfig := make(map[string]*auth.CollectionAccessConfig) + for collectionName, collection := range scope { + _, err := database.GetDatabaseCollection(scopeName, collectionName) + // collection doesn't exist anymore, but did at some point + if err != nil { + continue + } + collectionAccessConfig := &auth.CollectionAccessConfig{ + ExplicitChannels_: collection.ExplicitChannels().AsSet(), + } + if includeDynamicGrantInfo { + if user, ok := princ.(auth.User); ok { + collectionAccessConfig.Channels_ = user.InheritedCollectionChannels(scopeName, collectionName).AsSet() + } else { + collectionAccessConfig.Channels_ = princ.CollectionChannels(scopeName, collectionName).AsSet() + } + } + scopeAccessConfig[collectionName] = collectionAccessConfig + } + info.CollectionAccess[scopeName] = scopeAccessConfig + } + } + + if user, ok := princ.(auth.User); ok { + if includeDynamicGrantInfo { + info.Channels = user.InheritedCollectionChannels(base.DefaultScope, base.DefaultCollection).AsSet() + } + } else { + if includeDynamicGrantInfo { + info.Channels = princ.Channels().AsSet() + } + } + return info + +} + // Handles PUT and POST for a user or a role. func (h *handler) updatePrincipal(name string, isUser bool) error { h.assertAdminOnly() diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 5009bded5f..d2477f1dce 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -26,7 +26,7 @@ func (h *handler) handleGetAllChannels() error { if user == nil { return kNotFoundError } - info := marshalPrincipal(h.db, user, true) + info := marshalChannels(h.db, user, true) channels := allChannels{Channels: info.Channels} if !h.db.OnlyDefaultCollection() { From 583fa05e7991c8b7645756ac753adb13c49a4ff1 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Tue, 23 Jan 2024 11:02:37 +0000 Subject: [PATCH 16/26] Fix comment --- rest/admin_api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest/admin_api.go b/rest/admin_api.go index 48c97d21f1..bd0bb5c69a 100644 --- a/rest/admin_api.go +++ b/rest/admin_api.go @@ -1329,7 +1329,7 @@ func marshalPrincipal(database *db.Database, princ auth.Principal, includeDynami return info } -// marshalPrincipal outputs a PrincipalConfig in a format for REST API endpoints. +// marshalChannels outputs a list of channels in a format for REST API endpoints. func marshalChannels(database *db.Database, princ auth.Principal, includeDynamicGrantInfo bool) auth.PrincipalConfig { name := externalUserName(princ.Name()) info := auth.PrincipalConfig{ From a753f2e518a9cf5959e14a9c1746605abab3c0b7 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 9 May 2024 17:03:20 +0100 Subject: [PATCH 17/26] Rollback to pre-source implementation and remove tests --- db/database.go | 16 +++ rest/diagnostic_api.go | 40 ++++--- rest/diagnostic_api_test.go | 208 ------------------------------------ 3 files changed, 43 insertions(+), 221 deletions(-) diff --git a/db/database.go b/db/database.go index 8508e4bbf3..dc48c39209 100644 --- a/db/database.go +++ b/db/database.go @@ -2438,3 +2438,19 @@ func (dbc *DatabaseContext) InstallPrincipals(ctx context.Context, spec map[stri } return nil } + +// DataStoreNames returns the names of all datastore connected to this database +func (db *Database) DataStoreNames() base.ScopeAndCollectionNames { + if db.Scopes == nil { + return base.ScopeAndCollectionNames{ + base.DefaultScopeAndCollectionName(), + } + } + var names base.ScopeAndCollectionNames + for scopeName, scope := range db.Scopes { + for collectionName := range scope.Collections { + names = append(names, base.ScopeAndCollectionName{Scope: scopeName, Collection: collectionName}) + } + } + return names +} diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index d2477f1dce..675213edb4 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -9,12 +9,13 @@ package rest import ( + "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" "github.com/gorilla/mux" ) type allChannels struct { - Channels base.Set `json:"all_channels,omitempty"` + Channels map[string]map[string]auth.GrantHistory `json:"all_channels,omitempty"` } func (h *handler) handleGetAllChannels() error { @@ -26,21 +27,34 @@ func (h *handler) handleGetAllChannels() error { if user == nil { return kNotFoundError } - info := marshalChannels(h.db, user, true) - - channels := allChannels{Channels: info.Channels} - if !h.db.OnlyDefaultCollection() { - allCollectionChannels := make(map[string]map[string]allChannels) - for scope, collectionConfig := range info.CollectionAccess { - for collectionName, CAConfig := range collectionConfig { - allCollectionChannels[scope] = make(map[string]allChannels) - allCollectionChannels[scope][collectionName] = allChannels{Channels: CAConfig.Channels_} + + resp := make(map[string]map[string]auth.GrantHistory) + + // handles deleted collections, default/ single named collection + for _, dsName := range h.db.DataStoreNames() { + keyspace := dsName.ScopeName() + "." + dsName.CollectionName() + + resp[keyspace] = make(map[string]auth.GrantHistory) + channels := user.CollectionChannels(dsName.ScopeName(), dsName.CollectionName()) + chanHistory := user.CollectionChannelHistory(dsName.ScopeName(), dsName.CollectionName()) + for chanName, chanEntry := range channels { + resp[keyspace][chanName] = auth.GrantHistory{Entries: []auth.GrantHistorySequencePair{{StartSeq: chanEntry.Sequence, EndSeq: 0}}} + } + for chanName, chanEntry := range chanHistory { + chanHistoryEntry := auth.GrantHistory{Entries: chanEntry.Entries, UpdatedAt: chanEntry.UpdatedAt} + // if channel is also in history, append current entry to history entries + if _, chanCurrentlyAssigned := resp[chanName]; chanCurrentlyAssigned { + var newEntries []auth.GrantHistorySequencePair + copy(newEntries, chanHistory[chanName].Entries) + newEntries = append(newEntries, chanEntry.Entries...) + chanHistoryEntry.Entries = newEntries } + resp[keyspace][chanName] = chanHistoryEntry } - bytes, err := base.JSONMarshal(info.CollectionAccess) - h.writeRawJSON(bytes) - return err + } + channels := allChannels{Channels: resp} + bytes, err := base.JSONMarshal(channels) h.writeRawJSON(bytes) return err diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index d1ab454875..4ff59cc322 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -7,211 +7,3 @@ // the file licenses/APL2.txt. package rest - -import ( - "encoding/json" - "fmt" - "net/http" - "testing" - - "github.com/couchbase/sync_gateway/auth" - - "github.com/couchbase/sync_gateway/base" - "github.com/stretchr/testify/assert" - - "github.com/stretchr/testify/require" -) - -func TestGetAllChannelsByUser(t *testing.T) { - if base.TestsUseNamedCollections() { - t.Skip("Test requires Couchbase Server") - } - - rt := NewRestTester(t, &RestTesterConfig{ - PersistentConfig: true, - - SyncFn: `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel); role(doc.user, doc.role);}`, - }) - defer rt.Close() - - dbConfig := rt.NewDbConfig() - - dbName := "db" - rt.CreateDatabase("db", dbConfig) - - // create sessions before users - const alice = "alice" - const bob = "bob" - - // Put user alice and assert admin assigned channels are returned by the get all_channels endpoint - response := rt.SendAdminRequest(http.MethodPut, - "/"+dbName+"/_user/"+alice, - `{"name": "`+alice+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": ["A","B","C"]}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+alice+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - - var channelMap allChannels - err := json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!"}) - - // Assert non existent user returns 404 - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusNotFound) - - // Put user bob and assert on channels returned by all_channels - response = rt.SendAdminRequest(http.MethodPut, - "/"+dbName+"/_user/"+bob, - `{"name": "`+bob+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": []}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!"}) - - // Assign new channel to user bob and assert all_channels includes it - response = rt.SendAdminRequest(http.MethodPut, - "/{{.keyspace}}/doc1", - `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!", "NewChannel"}) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+alice+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) - - response = rt.SendAdminRequest("PUT", "/db/_role/role1", `{"admin_channels":["chan"]}`) - RequireStatus(t, response, http.StatusCreated) - - // Assign new channel to user bob and assert all_channels includes it - response = rt.SendAdminRequest(http.MethodPut, - "/{{.keyspace}}/doc2", - `{"role":"role:role1", "user":"bob"}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap.Channels.ToArray(), []string{"!", "NewChannel", "chan"}) - -} - -func TestGetAllChannelsByUserWithCollections(t *testing.T) { - SyncFn := `function(doc) {channel(doc.channel); access(doc.accessUser, doc.accessChannel);}` - - rt := NewRestTester(t, &RestTesterConfig{ - PersistentConfig: true, - }) - defer rt.Close() - - dbName := "db" - tb := base.GetTestBucket(t) - defer tb.Close(rt.Context()) - scopesConfig := GetCollectionsConfig(t, tb, 2) - for scope, _ := range scopesConfig { - for _, cnf := range scopesConfig[scope].Collections { - cnf.SyncFn = &SyncFn - } - } - dbConfig := makeDbConfig(tb.GetName(), dbName, scopesConfig) - rt.CreateDatabase("db", dbConfig) - - scopeName := rt.GetDbCollections()[0].ScopeName - collection1Name := rt.GetDbCollections()[0].Name - collection2Name := rt.GetDbCollections()[1].Name - scopesConfig[scopeName].Collections[collection1Name] = &CollectionConfig{} - - collectionPayload := fmt.Sprintf(`,"%s": { - "admin_channels":["a"] - }`, collection2Name) - - // create sessions before users - const alice = "alice" - const bob = "bob" - userPayload := `{ - %s - "admin_channels":["foo", "bar"], - "collection_access": { - "%s": { - "%s": { - "admin_channels":["A", "B", "C"] - } - %s - } - } - }` - // Put user alice and assert admin assigned channels are returned by the get all_channels endpoint - response := rt.SendAdminRequest(http.MethodPut, - "/"+dbName+"/_user/"+alice, fmt.Sprintf(userPayload, `"email":"bob@couchbase.com","password":"letmein",`, - scopeName, collection1Name, collectionPayload)) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+alice+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - - var channelMap map[string]map[string]*auth.CollectionAccessConfig - err := json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"A", "B", "C", "!"}) - - // Assert non existent user returns 404 - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusNotFound) - - // Put user bob and assert on channels returned by all_channels - response = rt.SendAdminRequest(http.MethodPut, - "/"+dbName+"/_user/"+bob, - `{"name": "`+bob+`", "password": "`+RestTesterDefaultUserPassword+`", "admin_channels": []}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!"}) - - // Assign new channel to user bob and assert all_channels includes it - response = rt.SendAdminRequest(http.MethodPut, - fmt.Sprintf("/%s/%s", rt.GetKeyspaces()[0], "doc1"), - `{"accessChannel":"NewChannel", "accessUser":["bob","alice"]}`) - RequireStatus(t, response, http.StatusCreated) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+bob+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - err = json.Unmarshal(response.BodyBytes(), &channelMap) - - require.NoError(t, err) - assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"!", "NewChannel"}) - - response = rt.SendAdminRequest(http.MethodGet, - "/"+dbName+"/_user/"+alice+"/all_channels", ``) - RequireStatus(t, response, http.StatusOK) - - err = json.Unmarshal(response.BodyBytes(), &channelMap) - require.NoError(t, err) - assert.ElementsMatch(t, channelMap[scopeName][collection1Name].Channels_.ToArray(), []string{"A", "B", "C", "!", "NewChannel"}) -} From 34e1726f2199610ffa500f97d201475bf2d94698 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Wed, 15 May 2024 15:18:52 +0100 Subject: [PATCH 18/26] Add most test cases, 404 and last 6 in spreadsheet left --- rest/diagnostic_api.go | 7 +- rest/diagnostic_api_test.go | 622 ++++++++++++++++++++++++++++++++++++ rest/routing.go | 5 +- 3 files changed, 627 insertions(+), 7 deletions(-) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 675213edb4..c353fcbb69 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -35,7 +35,7 @@ func (h *handler) handleGetAllChannels() error { keyspace := dsName.ScopeName() + "." + dsName.CollectionName() resp[keyspace] = make(map[string]auth.GrantHistory) - channels := user.CollectionChannels(dsName.ScopeName(), dsName.CollectionName()) + channels := user.InheritedCollectionChannels(dsName.ScopeName(), dsName.CollectionName()) chanHistory := user.CollectionChannelHistory(dsName.ScopeName(), dsName.CollectionName()) for chanName, chanEntry := range channels { resp[keyspace][chanName] = auth.GrantHistory{Entries: []auth.GrantHistorySequencePair{{StartSeq: chanEntry.Sequence, EndSeq: 0}}} @@ -43,10 +43,9 @@ func (h *handler) handleGetAllChannels() error { for chanName, chanEntry := range chanHistory { chanHistoryEntry := auth.GrantHistory{Entries: chanEntry.Entries, UpdatedAt: chanEntry.UpdatedAt} // if channel is also in history, append current entry to history entries - if _, chanCurrentlyAssigned := resp[chanName]; chanCurrentlyAssigned { + if _, chanCurrentlyAssigned := resp[keyspace][chanName]; chanCurrentlyAssigned { var newEntries []auth.GrantHistorySequencePair - copy(newEntries, chanHistory[chanName].Entries) - newEntries = append(newEntries, chanEntry.Entries...) + newEntries = append(chanEntry.Entries, resp[keyspace][chanName].Entries...) chanHistoryEntry.Entries = newEntries } resp[keyspace][chanName] = chanHistoryEntry diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 4ff59cc322..94a24ec696 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -7,3 +7,625 @@ // the file licenses/APL2.txt. package rest + +import ( + "encoding/json" + "fmt" + "github.com/couchbase/sync_gateway/auth" + "github.com/couchbase/sync_gateway/base" + "github.com/stretchr/testify/require" + "net/http" + "strings" + "testing" +) + +type grant interface { + request(rt *RestTester) +} + +const fakeUpdatedTime = 1234 + +// convertUpdatedTimeToConstant replaces the updated_at field in the response with a constant value to be able to diff +func convertUpdatedTimeToConstant(t testing.TB, output []byte) string { + var channelMap allChannels + err := json.Unmarshal(output, &channelMap) + require.NoError(t, err) + for keyspace, channels := range channelMap.Channels { + for channelName, grant := range channels { + if grant.UpdatedAt == 0 { + continue + } + grant.UpdatedAt = fakeUpdatedTime + channelMap.Channels[keyspace][channelName] = grant + } + } + + fmt.Printf("Converted response: %s\n", string(base.MustJSONMarshal(t, channelMap))) + return string(base.MustJSONMarshal(t, channelMap)) +} + +func compareAllChannelsOutput(rt *RestTester, username string, expectedOutput string) { + response := rt.SendDiagnosticRequest(http.MethodGet, + "/{{.db}}/_user/"+username+"/_all_channels", ``) + RequireStatus(rt.TB, response, http.StatusOK) + rt.TB.Logf("All channels response: %s", response.BodyString()) + require.JSONEq(rt.TB, expectedOutput, convertUpdatedTimeToConstant(rt.TB, response.BodyBytes())) + +} + +type userGrant struct { + user string + adminChannels map[string][]string + roles []string + output string +} + +func (g *userGrant) getUserPayload(t testing.TB) string { + config := auth.PrincipalConfig{ + Name: base.StringPtr(g.user), + Password: base.StringPtr(RestTesterDefaultUserPassword), + } + if len(g.roles) > 0 { + config.ExplicitRoleNames = base.SetOf(g.roles...) + } + + for keyspace, chans := range g.adminChannels { + scopeName, collectionName := strings.Split(keyspace, ".")[0], strings.Split(keyspace, ".")[1] + if base.IsDefaultCollection(scopeName, collectionName) { + config.ExplicitChannels = base.SetFromArray(chans) + } else { + config.SetExplicitChannels(scopeName, collectionName, chans...) + } + } + + return string(base.MustJSONMarshal(t, config)) +} + +func (g userGrant) request(rt *RestTester) { + payload := g.getUserPayload(rt.TB) + rt.TB.Logf("Issuing admin grant: %+v", payload) + response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/_user/"+g.user, payload) + if response.Code != http.StatusCreated && response.Code != http.StatusOK { + rt.TB.Fatalf("Expected 200 or 201 exit code") + } + if g.output != "" { + compareAllChannelsOutput(rt, g.user, g.output) + } +} + +type roleGrant struct { + role string + adminChannels map[string][]string +} + +func (g roleGrant) getPayload(t testing.TB) string { + config := auth.PrincipalConfig{ + //Name: base.StringPtr(g.role), + Password: base.StringPtr(RestTesterDefaultUserPassword), + } + for keyspace, chans := range g.adminChannels { + scopeName, collectionName := strings.Split(keyspace, ".")[0], strings.Split(keyspace, ".")[1] + if base.IsDefaultCollection(scopeName, collectionName) { + config.ExplicitChannels = base.SetFromArray(chans) + } else { + config.SetExplicitChannels(scopeName, collectionName, chans...) + } + } + return string(base.MustJSONMarshal(t, config)) +} + +func (g roleGrant) request(rt *RestTester) { + payload := g.getPayload(rt.TB) + rt.TB.Logf("Issuing admin grant: %+v", payload) + response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/_role/"+g.role, payload) + if response.Code != http.StatusCreated && response.Code != http.StatusOK { + rt.TB.Fatalf("Expected 200 or 201 exit code") + } +} + +type docGrant struct { + userName string + dynamicRoles string + dynamicChannels string + output string +} + +func (g docGrant) getPayload(t testing.TB) string { + role := fmt.Sprintf(`"role":"role:%s",`, g.dynamicRoles) + if g.dynamicRoles == "" { + role = "" + } + user := fmt.Sprintf(`"user":["%s"],`, g.userName) + if g.userName == "" { + user = "" + } + payload := fmt.Sprintf(`{%s %s "channel":"%s"}`, user, role, g.dynamicChannels) + + return payload +} + +func (g docGrant) request(rt *RestTester) { + payload := g.getPayload(rt.TB) + rt.TB.Logf("Issuing dynamic grant: %+v", payload) + response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/doc", payload) + if response.Code != http.StatusCreated && response.Code != http.StatusOK { + rt.TB.Fatalf("Expected 200 or 201 exit code") + } + if g.output != "" { + compareAllChannelsOutput(rt, g.userName, g.output) + } +} + +func TestGetAllChannelsExample(t *testing.T) { + defaultKeyspace := "_default._default" + tests := []struct { + name string + adminChannels []string + grants []grant + }{ + { + name: "admin channels once", + grants: []grant{ + // grant 1 + userGrant{ + user: "alice", + adminChannels: map[string][]string{ + defaultKeyspace: {"A", "B", "C"}, + }, + output: ` +{"all_channels":{"_default._default": { + "A": { "entries" : ["1-0"], "updated_at":0}, + "B": { "entries" : ["1-0"], "updated_at":0}, + "C": { "entries" : ["1-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }}}, + { + name: "multiple history entries", + grants: []grant{ + // grant 1 + userGrant{ + user: "alice", + adminChannels: map[string][]string{ + defaultKeyspace: {"A"}, + }, + output: ` +{"all_channels":{"_default._default": { + "A": { "entries" : ["1-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + // grant 2 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + output: ` +{"all_channels":{"_default._default": { + "A": { "entries" : ["1-2"], "updated_at":1234}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + // grant 2 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` +{"all_channels":{"_default._default": { + "A": { "entries" : ["1-2", "3-0"], "updated_at":1234}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "limit history entries to 10", + grants: []grant{ + // grant 1 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 2 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 3 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 4 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 5 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 6 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 7 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 8 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 9 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 10 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 11 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 12 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 13 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 14 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 15 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 16 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 17 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 18 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 19 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 20 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 19 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + // grant 20 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + }, + // grant 23 + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"], "updated_at":1234}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "admin role grant channels", + grants: []grant{ + // grant 1 + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A", "B"}}, + }, + userGrant{ + user: "alice", + roles: []string{"role1"}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["2-0"], "updated_at":0}, + "B": { "entries" : ["2-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "dynamic grant channels", + grants: []grant{ + userGrant{ + user: "alice", + }, + docGrant{ + userName: "alice", + dynamicChannels: "A", + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["2-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "dynamic role grant channels", + grants: []grant{ + userGrant{ + user: "alice", + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A", "B"}}, + }, + docGrant{ + userName: "alice", + dynamicRoles: "role1", + dynamicChannels: "chan1", + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["3-0"], "updated_at":0}, + "B": { "entries" : ["3-0"], "updated_at":0}, + "chan1": { "entries" : ["3-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both dynamic and admin grants, assert earlier sequence (admin) is used", + grants: []grant{ + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + docGrant{ + userName: "alice", + dynamicChannels: "A", + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["1-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both dynamic and admin grants, assert earlier sequence (dynamic) is used", + grants: []grant{ + userGrant{ + user: "alice", + }, + docGrant{ + userName: "alice", + dynamicChannels: "A", + }, + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["2-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both admin and admin role grants, assert earlier sequence (admin) is used", + grants: []grant{ + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + userGrant{ + user: "alice", + roles: []string{"role1"}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["1-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both admin and admin role grants, assert earlier sequence (admin role) is used", + grants: []grant{ + userGrant{ + user: "alice", + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + userGrant{ + user: "alice", + roles: []string{"role1"}, + }, + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["3-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, // A start sequence would be 4 if its broken + }, + }, + }, + { + name: "channel assigned through both dynamic role and admin grants, assert earlier sequence (dynamic role) is used", + grants: []grant{ + userGrant{ + user: "alice", + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + docGrant{ + userName: "alice", + dynamicRoles: "role1", + dynamicChannels: "docChan", + }, + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["3-0"], "updated_at":0}, + "docChan": { "entries" : ["3-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both dynamic role and admin grants, assert earlier sequence (admin) is used", + grants: []grant{ + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + docGrant{ + userName: "alice", + dynamicRoles: "role1", + dynamicChannels: "docChan", + }, + userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["1-0"], "updated_at":0}, + "docChan": { "entries" : ["3-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both dynamic role and admin role grants, assert earlier sequence (dynamic role) is used", + grants: []grant{ + userGrant{ + user: "alice", + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + roleGrant{ + role: "role2", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + docGrant{ + userName: "alice", + dynamicRoles: "role1", + dynamicChannels: "docChan", + }, + userGrant{ + user: "alice", + roles: []string{"role2"}, + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["4-0"], "updated_at":0}, + "docChan": { "entries" : ["4-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + { + name: "channel assigned through both dynamic role and admin role grants, assert earlier sequence (admin role) is used", + grants: []grant{ + userGrant{ + user: "alice", + }, + roleGrant{ + role: "role1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + roleGrant{ + role: "role2", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + }, + userGrant{ + user: "alice", + roles: []string{"role2"}, + }, + docGrant{ + userName: "alice", + dynamicRoles: "role1", + dynamicChannels: "docChan", + output: ` + {"all_channels":{"_default._default": { + "A": { "entries" : ["4-0"], "updated_at":0}, + "docChan": { "entries" : ["5-0"], "updated_at":0}, + "!": { "entries" : ["1-0"], "updated_at":0} + }}}`, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + rt := NewRestTester(t, &RestTesterConfig{ + PersistentConfig: true, + }) + defer rt.Close() + + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = nil + dbConfig.Sync = base.StringPtr(`function(doc) {channel(doc.channel); access(doc.user, doc.channel); role(doc.user, doc.role);}`) + rt.CreateDatabase("db", dbConfig) + + // create user with adminChannels1 + for i, grant := range test.grants { + t.Logf("Processing grant %d", i+1) + grant.request(rt) + } + + }) + } + +} diff --git a/rest/routing.go b/rest/routing.go index d60daee1d1..6487d5cd05 100644 --- a/rest/routing.go +++ b/rest/routing.go @@ -183,9 +183,6 @@ func CreateAdminRouter(sc *ServerContext) *mux.Router { dbr.Handle("/_user/{name}", makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUser)).Methods("DELETE") - dbr.Handle("/_user/{name}/all_channels", - makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).handleGetAllChannels)).Methods("GET") - dbr.Handle("/_user/{name}/_session", makeHandler(sc, adminPrivs, []Permission{PermWritePrincipal}, nil, (*handler).deleteUserSessions)).Methods("DELETE") dbr.Handle("/_user/{name}/_session/{sessionid}", @@ -378,6 +375,8 @@ func createDiagnosticRouter(sc *ServerContext) *mux.Router { keyspace.Handle("/{docid:"+docRegex+"}/_all_channels", makeHandler(sc, adminPrivs, []Permission{PermReadAppData}, nil, (*handler).handleGetDocChannels)).Methods("GET") keyspace.Handle("/_sync", makeHandler(sc, adminPrivs, []Permission{PermReadAppData}, nil, (*handler).handleSyncFnDryRun)).Methods("GET") keyspace.Handle("/_import_filter", makeHandler(sc, adminPrivs, []Permission{PermReadAppData}, nil, (*handler).handleImportFilterDryRun)).Methods("GET") + dbr.Handle("/_user/{name}/_all_channels", + makeHandler(sc, adminPrivs, []Permission{PermReadPrincipal}, nil, (*handler).handleGetAllChannels)).Methods("GET") return r } From 0c9be9023e7e258e943dcf1b1d660e96e39e9e3b Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 16 May 2024 14:10:16 +0100 Subject: [PATCH 19/26] Add remaining test cases and remove unused code --- rest/admin_api.go | 48 ------ rest/diagnostic_api.go | 17 +- rest/diagnostic_api_test.go | 324 ++++++++++++++++++++++++++++++++---- 3 files changed, 302 insertions(+), 87 deletions(-) diff --git a/rest/admin_api.go b/rest/admin_api.go index bd0bb5c69a..c36c22b163 100644 --- a/rest/admin_api.go +++ b/rest/admin_api.go @@ -1329,54 +1329,6 @@ func marshalPrincipal(database *db.Database, princ auth.Principal, includeDynami return info } -// marshalChannels outputs a list of channels in a format for REST API endpoints. -func marshalChannels(database *db.Database, princ auth.Principal, includeDynamicGrantInfo bool) auth.PrincipalConfig { - name := externalUserName(princ.Name()) - info := auth.PrincipalConfig{ - Name: &name, - ExplicitChannels: princ.ExplicitChannels().AsSet(), - } - - collectionAccess := princ.GetCollectionsAccess() - if collectionAccess != nil && !database.OnlyDefaultCollection() { - info.CollectionAccess = make(map[string]map[string]*auth.CollectionAccessConfig) - for scopeName, scope := range collectionAccess { - scopeAccessConfig := make(map[string]*auth.CollectionAccessConfig) - for collectionName, collection := range scope { - _, err := database.GetDatabaseCollection(scopeName, collectionName) - // collection doesn't exist anymore, but did at some point - if err != nil { - continue - } - collectionAccessConfig := &auth.CollectionAccessConfig{ - ExplicitChannels_: collection.ExplicitChannels().AsSet(), - } - if includeDynamicGrantInfo { - if user, ok := princ.(auth.User); ok { - collectionAccessConfig.Channels_ = user.InheritedCollectionChannels(scopeName, collectionName).AsSet() - } else { - collectionAccessConfig.Channels_ = princ.CollectionChannels(scopeName, collectionName).AsSet() - } - } - scopeAccessConfig[collectionName] = collectionAccessConfig - } - info.CollectionAccess[scopeName] = scopeAccessConfig - } - } - - if user, ok := princ.(auth.User); ok { - if includeDynamicGrantInfo { - info.Channels = user.InheritedCollectionChannels(base.DefaultScope, base.DefaultCollection).AsSet() - } - } else { - if includeDynamicGrantInfo { - info.Channels = princ.Channels().AsSet() - } - } - return info - -} - // Handles PUT and POST for a user or a role. func (h *handler) updatePrincipal(name string, isUser bool) error { h.assertAdminOnly() diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index c353fcbb69..490b1b6c77 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -11,6 +11,7 @@ package rest import ( "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" + channels "github.com/couchbase/sync_gateway/channels" "github.com/gorilla/mux" ) @@ -34,10 +35,16 @@ func (h *handler) handleGetAllChannels() error { for _, dsName := range h.db.DataStoreNames() { keyspace := dsName.ScopeName() + "." + dsName.CollectionName() - resp[keyspace] = make(map[string]auth.GrantHistory) - channels := user.InheritedCollectionChannels(dsName.ScopeName(), dsName.CollectionName()) + currentChannels := user.InheritedCollectionChannels(dsName.ScopeName(), dsName.CollectionName()) chanHistory := user.CollectionChannelHistory(dsName.ScopeName(), dsName.CollectionName()) - for chanName, chanEntry := range channels { + // If no channels aside from public and no channels in history, don't make a key for this keyspace + if len(currentChannels) > 1 || len(chanHistory) != 0 { + resp[keyspace] = make(map[string]auth.GrantHistory) + } + for chanName, chanEntry := range currentChannels { + if chanName == channels.DocumentStarChannel { + continue + } resp[keyspace][chanName] = auth.GrantHistory{Entries: []auth.GrantHistorySequencePair{{StartSeq: chanEntry.Sequence, EndSeq: 0}}} } for chanName, chanEntry := range chanHistory { @@ -52,9 +59,9 @@ func (h *handler) handleGetAllChannels() error { } } - channels := allChannels{Channels: resp} + allChannels := allChannels{Channels: resp} - bytes, err := base.JSONMarshal(channels) + bytes, err := base.JSONMarshal(allChannels) h.writeRawJSON(bytes) return err } diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 94a24ec696..b134392434 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -13,6 +13,7 @@ import ( "fmt" "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" "strings" @@ -23,6 +24,7 @@ type grant interface { request(rt *RestTester) } +const defaultKeyspace = "_default._default" const fakeUpdatedTime = 1234 // convertUpdatedTimeToConstant replaces the updated_at field in the response with a constant value to be able to diff @@ -156,8 +158,7 @@ func (g docGrant) request(rt *RestTester) { } } -func TestGetAllChannelsExample(t *testing.T) { - defaultKeyspace := "_default._default" +func TestGetAllChannelsByUser(t *testing.T) { tests := []struct { name string adminChannels []string @@ -176,8 +177,7 @@ func TestGetAllChannelsExample(t *testing.T) { {"all_channels":{"_default._default": { "A": { "entries" : ["1-0"], "updated_at":0}, "B": { "entries" : ["1-0"], "updated_at":0}, - "C": { "entries" : ["1-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "C": { "entries" : ["1-0"], "updated_at":0} }}}`, }}}, { @@ -191,8 +191,7 @@ func TestGetAllChannelsExample(t *testing.T) { }, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"], "updated_at":0} }}}`, }, // grant 2 @@ -201,8 +200,7 @@ func TestGetAllChannelsExample(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"!"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-2"], "updated_at":1234}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-2"], "updated_at":1234} }}}`, }, // grant 2 @@ -211,8 +209,7 @@ func TestGetAllChannelsExample(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-2", "3-0"], "updated_at":1234}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-2", "3-0"], "updated_at":1234} }}}`, }, }, @@ -336,8 +333,7 @@ func TestGetAllChannelsExample(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"], "updated_at":1234}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"], "updated_at":1234} }}}`, }, }, @@ -356,8 +352,7 @@ func TestGetAllChannelsExample(t *testing.T) { output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["2-0"], "updated_at":0}, - "B": { "entries" : ["2-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "B": { "entries" : ["2-0"], "updated_at":0} }}}`, }, }, @@ -373,8 +368,7 @@ func TestGetAllChannelsExample(t *testing.T) { dynamicChannels: "A", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["2-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["2-0"], "updated_at":0} }}}`, }, }, @@ -397,8 +391,7 @@ func TestGetAllChannelsExample(t *testing.T) { {"all_channels":{"_default._default": { "A": { "entries" : ["3-0"], "updated_at":0}, "B": { "entries" : ["3-0"], "updated_at":0}, - "chan1": { "entries" : ["3-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "chan1": { "entries" : ["3-0"], "updated_at":0} }}}`, }, }, @@ -415,8 +408,7 @@ func TestGetAllChannelsExample(t *testing.T) { dynamicChannels: "A", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"], "updated_at":0} }}}`, }, }, @@ -436,8 +428,7 @@ func TestGetAllChannelsExample(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["2-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["2-0"], "updated_at":0} }}}`, }, }, @@ -458,8 +449,7 @@ func TestGetAllChannelsExample(t *testing.T) { roles: []string{"role1"}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"], "updated_at":0} }}}`, }, }, @@ -483,8 +473,7 @@ func TestGetAllChannelsExample(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["3-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["3-0"], "updated_at":0} }}}`, // A start sequence would be 4 if its broken }, }, @@ -510,8 +499,7 @@ func TestGetAllChannelsExample(t *testing.T) { output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["3-0"], "updated_at":0}, - "docChan": { "entries" : ["3-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "docChan": { "entries" : ["3-0"], "updated_at":0} }}}`, }, }, @@ -538,8 +526,7 @@ func TestGetAllChannelsExample(t *testing.T) { output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["1-0"], "updated_at":0}, - "docChan": { "entries" : ["3-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "docChan": { "entries" : ["3-0"], "updated_at":0} }}}`, }, }, @@ -569,8 +556,7 @@ func TestGetAllChannelsExample(t *testing.T) { output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["4-0"], "updated_at":0}, - "docChan": { "entries" : ["4-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "docChan": { "entries" : ["4-0"], "updated_at":0} }}}`, }, }, @@ -600,8 +586,7 @@ func TestGetAllChannelsExample(t *testing.T) { output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["4-0"], "updated_at":0}, - "docChan": { "entries" : ["5-0"], "updated_at":0}, - "!": { "entries" : ["1-0"], "updated_at":0} + "docChan": { "entries" : ["5-0"], "updated_at":0} }}}`, }, }, @@ -627,5 +612,276 @@ func TestGetAllChannelsExample(t *testing.T) { }) } +} + +func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { + base.TestRequiresCollections(t) + + rt := NewRestTesterMultipleCollections(t, &RestTesterConfig{PersistentConfig: true}, 1) + defer rt.Close() + + const dbName = "db" + + // implicit default scope/collection + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = nil + resp := rt.CreateDatabase(dbName, dbConfig) + RequireStatus(t, resp, http.StatusCreated) + + expectedKeyspaces := []string{ + dbName, + } + assert.Equal(t, expectedKeyspaces, rt.GetKeyspaces()) + + newCollection := base.ScopeAndCollectionName{Scope: base.DefaultScope, Collection: t.Name()} + require.NoError(t, rt.TestBucket.CreateDataStore(base.TestCtx(t), newCollection)) + defer func() { + require.NoError(t, rt.TestBucket.DropDataStore(newCollection)) + }() + + resp = rt.UpsertDbConfig(dbName, DbConfig{Scopes: ScopesConfig{ + base.DefaultScope: {Collections: CollectionsConfig{ + base.DefaultCollection: {}, + newCollection.CollectionName(): {}, + }}, + }}) + RequireStatus(t, resp, http.StatusCreated) + + // Test that the keyspace with no channels assigned does not have a key in the response + grant := userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {}, newCollection.String(): {"D"}}, + output: fmt.Sprintf(` + { + "all_channels":{ + "%s":{ + "D":{ + "entries":["1-0"], + "updated_at":0 + } + } + }}`, newCollection.String()), + } + grant.request(rt) + + // check single named collection is handled + grant = userGrant{ + user: "alice", + adminChannels: map[string][]string{defaultKeyspace: {"A"}, newCollection.String(): {"D"}}, + output: fmt.Sprintf(` +{ + "all_channels":{ + "_default._default":{ + "A":{ + "entries":["2-0"], + "updated_at":0 + } + }, + "%s":{ + "D":{ + "entries":[ + "1-0" + ], + "updated_at":0 + } + } + } +}`, newCollection.String()), + } + grant.request(rt) + +} + +func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { + base.TestRequiresCollections(t) + + rt := NewRestTesterMultipleCollections(t, &RestTesterConfig{PersistentConfig: true}, 2) + defer rt.Close() + + dbName := "db" + tb := base.GetTestBucket(t) + defer tb.Close(rt.Context()) + scopesConfig := GetCollectionsConfig(t, tb, 2) + + dbConfig := makeDbConfig(tb.GetName(), dbName, scopesConfig) + rt.CreateDatabase("db", dbConfig) + + scopeName := rt.GetDbCollections()[0].ScopeName + collection1Name := rt.GetDbCollections()[0].Name + collection2Name := rt.GetDbCollections()[1].Name + scopesConfig[scopeName].Collections[collection1Name] = &CollectionConfig{} + keyspace1 := scopeName + "." + collection1Name + keyspace2 := scopeName + "." + collection2Name + + // Test that the keyspace with no channels assigned does not have a key in the response + grant := userGrant{ + user: "alice", + adminChannels: map[string][]string{keyspace1: {"D"}}, + output: fmt.Sprintf(` + { + "all_channels":{ + "%s":{ + "D":{ + "entries":["1-0"], + "updated_at":0 + } + } + }}`, keyspace1), + } + grant.request(rt) + + // check single named collection is handled + grant = userGrant{ + user: "alice", + adminChannels: map[string][]string{keyspace2: {"A"}, keyspace1: {"D"}}, + output: fmt.Sprintf(` + { + "all_channels":{ + "%s":{ + "A":{ + "entries":["2-0"], + "updated_at":0 + } + }, + "%s":{ + "D":{ + "entries":[ + "1-0" + ], + "updated_at":0 + } + } + } + }`, keyspace2, keyspace1), + } + grant.request(rt) + + // check removed channel in keyspace2 is in history before deleting collection 2 + grant = userGrant{ + user: "alice", + adminChannels: map[string][]string{keyspace1: {"D"}, keyspace2: {"!"}}, + output: fmt.Sprintf(` + { + "all_channels":{ + "%s":{ + "A":{ + "entries":["2-3"], + "updated_at":1234 + } + }, + "%s":{ + "D":{ + "entries":["1-0"], + "updated_at":0 + } + } + } + }`, keyspace2, keyspace1), + } + grant.request(rt) + + // delete collection 2 + scopesConfig = GetCollectionsConfig(t, tb, 1) + dbConfig = makeDbConfig(tb.GetName(), dbName, scopesConfig) + rt.UpsertDbConfig("db", dbConfig) + + // check deleted collection is not there + grant = userGrant{ + user: "alice", + adminChannels: map[string][]string{keyspace1: {"D"}}, + output: fmt.Sprintf(` + { + "all_channels":{ + "%s":{ + "D":{ + "entries":[ + "1-0" + ], + "updated_at":0 + } + } + } + }`, keyspace1), + } + grant.request(rt) +} + +func TestGetAllChannelsByUserDeletedRole(t *testing.T) { + + rt := NewRestTester(t, &RestTesterConfig{ + PersistentConfig: true, + }) + defer rt.Close() + + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = nil + rt.CreateDatabase("db", dbConfig) + + // Create role with 1 channel and assign it to user + roleGrant := roleGrant{role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"role1Chan"}}} + userGrant := userGrant{ + user: "alice", + roles: []string{"role1"}, + output: ` + { + "all_channels":{ + "_default._default":{ + "role1Chan":{ + "entries":["2-0"], + "updated_at":0 + } + } + }}`, + } + roleGrant.request(rt) + userGrant.request(rt) + + resp := rt.SendAdminRequest("DELETE", "/db/_role/role1", ``) + RequireStatus(t, resp, http.StatusOK) + + // Delete role and assert its channels no longer appear in response + userGrant.output = `{}` + userGrant.roles = []string{} + userGrant.request(rt) + +} + +func TestGetAllChannelsByUserNonexistentAndDeletedUser(t *testing.T) { + + rt := NewRestTester(t, &RestTesterConfig{ + PersistentConfig: true, + }) + defer rt.Close() + + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = nil + rt.CreateDatabase("db", dbConfig) + // assert the endpoint returns 404 when user is not found + resp := rt.SendDiagnosticRequest("GET", "/db/_user/user1/_all_channels", ``) + RequireStatus(t, resp, http.StatusNotFound) + + // Create user and assert on response + userGrant := userGrant{ + user: "user1", + adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + output: ` + { + "all_channels":{ + "_default._default":{ + "A":{ + "entries":["1-0"], + "updated_at":0 + } + } + }}`, + } + userGrant.request(rt) + + // delete user + resp = rt.SendAdminRequest("DELETE", "/db/_user/user1", ``) + RequireStatus(t, resp, http.StatusOK) + // Get deleted user all channels, expect 404 + resp = rt.SendDiagnosticRequest("GET", "/db/_user/user1/_all_channels", ``) + RequireStatus(t, resp, http.StatusNotFound) } From c6f1aeb86ef07ca3712de1db31b6d88c87edb95c Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 16 May 2024 14:11:26 +0100 Subject: [PATCH 20/26] Fix error msg --- rest/diagnostic_api.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 490b1b6c77..03c00bb101 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -9,6 +9,7 @@ package rest import ( + "fmt" "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" channels "github.com/couchbase/sync_gateway/channels" @@ -23,7 +24,7 @@ func (h *handler) handleGetAllChannels() error { h.assertAdminOnly() user, err := h.db.Authenticator(h.ctx()).GetUser(internalUserName(mux.Vars(h.rq)["name"])) if err != nil { - return err + return fmt.Errorf("could not get user %s: %w", user.Name(), err) } if user == nil { return kNotFoundError From eb9953f09e98846855ee698ad24b580962a15d81 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 16 May 2024 14:33:55 +0100 Subject: [PATCH 21/26] Fix docs --- docs/api/admin.yaml | 2 -- docs/api/components/responses.yaml | 6 ++++ docs/api/components/schemas.yaml | 25 +++++++++++++++++ docs/api/diagnostic.yaml | 2 ++ .../admin/db-_user-name-all_channels.yaml | 28 ------------------- 5 files changed, 33 insertions(+), 30 deletions(-) delete mode 100644 docs/api/paths/admin/db-_user-name-all_channels.yaml diff --git a/docs/api/admin.yaml b/docs/api/admin.yaml index d057026a6d..145b752805 100644 --- a/docs/api/admin.yaml +++ b/docs/api/admin.yaml @@ -40,8 +40,6 @@ paths: $ref: './paths/admin/db-_user-.yaml' '/{db}/_user/{name}': $ref: './paths/admin/db-_user-name.yaml' - '/{db}/_user/{name}/all_channels': - $ref: './paths/admin/db-_user-name-all_channels.yaml' '/{db}/_user/{name}/_session': $ref: './paths/admin/db-_user-name-_session.yaml' '/{db}/_user/{name}/_session/{sessionid}': diff --git a/docs/api/components/responses.yaml b/docs/api/components/responses.yaml index 350c1638f9..21190b554a 100644 --- a/docs/api/components/responses.yaml +++ b/docs/api/components/responses.yaml @@ -180,3 +180,9 @@ DB-config-precondition-failed: example: error: Precondition Failed reason: Provided If-Match header does not match current config version +All_user_channels_response: + description: Map of all keyspaces to all channels that the user has access to in the, and their properties. + content: + application/json: + schema: + $ref: ./schemas.yaml#/All_user_channels_response diff --git a/docs/api/components/schemas.yaml b/docs/api/components/schemas.yaml index b25006c28f..3ae8bf971a 100644 --- a/docs/api/components/schemas.yaml +++ b/docs/api/components/schemas.yaml @@ -2512,3 +2512,28 @@ CollectionNames: - Starting - Stopping - Resyncing +All_user_channels_response: + description: |- + All user channels split by how they were assigned to the user and by keyspace. + type: object + properties: + all_channels: + description: |- + All channels that the user has access to. + type: object + properties: + keyspace: + type: object + properties: + channel: + $ref: '#/channelEntry' +channelEntry: + description: Channel name + type: object + properties: + entries: + type: array + description: Start sequence to end sequence + updated_at: + type: integer + description: Unix timestamp of last update diff --git a/docs/api/diagnostic.yaml b/docs/api/diagnostic.yaml index 24050a14ca..90b131189a 100644 --- a/docs/api/diagnostic.yaml +++ b/docs/api/diagnostic.yaml @@ -36,6 +36,8 @@ paths: $ref: './paths/diagnostic/keyspace-sync.yaml' '/{keyspace}/import_filter': $ref: './paths/diagnostic/keyspace-import_filter.yaml' + '/{db}/_user/{name}/all_channels': + $ref: './paths/diagnostic/db-_user-name-_all_channels.yaml' externalDocs: description: Sync Gateway Quickstart | Couchbase Docs url: 'https://docs.couchbase.com/sync-gateway/current/index.html' diff --git a/docs/api/paths/admin/db-_user-name-all_channels.yaml b/docs/api/paths/admin/db-_user-name-all_channels.yaml deleted file mode 100644 index ecb2cce6e9..0000000000 --- a/docs/api/paths/admin/db-_user-name-all_channels.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2022-Present Couchbase, Inc. -# -# Use of this software is governed by the Business Source License included -# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified -# in that file, in accordance with the Business Source License, use of this -# software will be governed by the Apache License, Version 2.0, included in -# the file licenses/APL2.txt. -parameters: - - $ref: ../../components/parameters.yaml#/db - - $ref: ../../components/parameters.yaml#/user-name -get: - summary: Get all channels for a user - description: |- - Retrieve all channels that a user has access to. - - Required Sync Gateway RBAC roles: - - * Sync Gateway Architect - * Sync Gateway Application - * Sync Gateway Application Read Only - responses: - '200': - $ref: ../../components/responses.yaml#/User - '404': - $ref: ../../components/responses.yaml#/Not-found - tags: - - Database Security - operationId: get_db-_user-name-all_channels From dc66efe46b7ea0b7bd1c1c20bcb05452ecc2420a Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 16 May 2024 14:53:39 +0100 Subject: [PATCH 22/26] Add comments and add missing doc file --- .../db-_user-name-_all_channels.yaml | 28 +++++ rest/diagnostic_api_test.go | 105 +++++++++++------- 2 files changed, 95 insertions(+), 38 deletions(-) create mode 100644 docs/api/paths/diagnostic/db-_user-name-_all_channels.yaml diff --git a/docs/api/paths/diagnostic/db-_user-name-_all_channels.yaml b/docs/api/paths/diagnostic/db-_user-name-_all_channels.yaml new file mode 100644 index 0000000000..c130a869e6 --- /dev/null +++ b/docs/api/paths/diagnostic/db-_user-name-_all_channels.yaml @@ -0,0 +1,28 @@ +# Copyright 2022-Present Couchbase, Inc. +# +# Use of this software is governed by the Business Source License included +# in the file licenses/BSL-Couchbase.txt. As of the Change Date specified +# in that file, in accordance with the Business Source License, use of this +# software will be governed by the Apache License, Version 2.0, included in +# the file licenses/APL2.txt. +parameters: + - $ref: ../../components/parameters.yaml#/db + - $ref: ../../components/parameters.yaml#/user-name +get: + summary: Get all channels for a user + description: |- + Retrieve all channels that a user has access to. + + Required Sync Gateway RBAC roles: + + * Sync Gateway Architect + * Sync Gateway Application + * Sync Gateway Application Read Only + responses: + '200': + $ref: ../../components/responses.yaml#/All_user_channels_response + '404': + $ref: ../../components/responses.yaml#/Not-found + tags: + - Database Security + operationId: get_db-_user-name_-all_channels diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index b134392434..295959372b 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -102,7 +102,6 @@ type roleGrant struct { func (g roleGrant) getPayload(t testing.TB) string { config := auth.PrincipalConfig{ - //Name: base.StringPtr(g.role), Password: base.StringPtr(RestTesterDefaultUserPassword), } for keyspace, chans := range g.adminChannels { @@ -126,22 +125,22 @@ func (g roleGrant) request(rt *RestTester) { } type docGrant struct { - userName string - dynamicRoles string - dynamicChannels string - output string + userName string + dynamicRole string + dynamicChannel string + output string } func (g docGrant) getPayload(t testing.TB) string { - role := fmt.Sprintf(`"role":"role:%s",`, g.dynamicRoles) - if g.dynamicRoles == "" { + role := fmt.Sprintf(`"role":"role:%s",`, g.dynamicRole) + if g.dynamicRole == "" { role = "" } user := fmt.Sprintf(`"user":["%s"],`, g.userName) if g.userName == "" { user = "" } - payload := fmt.Sprintf(`{%s %s "channel":"%s"}`, user, role, g.dynamicChannels) + payload := fmt.Sprintf(`{%s %s "channel":"%s"}`, user, role, g.dynamicChannel) return payload } @@ -364,8 +363,8 @@ func TestGetAllChannelsByUser(t *testing.T) { user: "alice", }, docGrant{ - userName: "alice", - dynamicChannels: "A", + userName: "alice", + dynamicChannel: "A", output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["2-0"], "updated_at":0} @@ -376,17 +375,20 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "dynamic role grant channels", grants: []grant{ + // create user userGrant{ user: "alice", }, + // create role with channels roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A", "B"}}, }, + // assign role through the sync fn and check output docGrant{ - userName: "alice", - dynamicRoles: "role1", - dynamicChannels: "chan1", + userName: "alice", + dynamicRole: "role1", + dynamicChannel: "chan1", output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["3-0"], "updated_at":0}, @@ -399,13 +401,15 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic and admin grants, assert earlier sequence (admin) is used", grants: []grant{ + // create user and assign channels through admin_channels userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign channels through sync fn and assert on sequences docGrant{ - userName: "alice", - dynamicChannels: "A", + userName: "alice", + dynamicChannel: "A", output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["1-0"], "updated_at":0} @@ -416,13 +420,16 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic and admin grants, assert earlier sequence (dynamic) is used", grants: []grant{ + // create user with no channels userGrant{ user: "alice", }, + // create doc and assign dynamic chan through sync fn docGrant{ - userName: "alice", - dynamicChannels: "A", + userName: "alice", + dynamicChannel: "A", }, + // assign same channels through admin_channels and assert on sequences userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, @@ -436,14 +443,17 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both admin and admin role grants, assert earlier sequence (admin) is used", grants: []grant{ + // create user and assign channel through admin_channels userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // create role with same channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign role through admin_roles and assert on sequences userGrant{ user: "alice", roles: []string{"role1"}, @@ -457,17 +467,21 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both admin and admin role grants, assert earlier sequence (admin role) is used", grants: []grant{ + // create user with no channels userGrant{ user: "alice", }, + // create role with channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign role through admin_roles userGrant{ user: "alice", roles: []string{"role1"}, }, + // assign role channel through admin_channels and assert on sequences userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, @@ -481,18 +495,22 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic role and admin grants, assert earlier sequence (dynamic role) is used", grants: []grant{ + // create user with no channels userGrant{ user: "alice", }, + // create role with channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // create doc and assign role through sync fn docGrant{ - userName: "alice", - dynamicRoles: "role1", - dynamicChannels: "docChan", + userName: "alice", + dynamicRole: "role1", + dynamicChannel: "docChan", }, + // assign role cahnnel to user through admin_channels and assert on sequences userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, @@ -507,22 +525,21 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic role and admin grants, assert earlier sequence (admin) is used", grants: []grant{ + // create user with channel userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // create role with same channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign role to user through sync fn and assert channel sequence is from admin_channels docGrant{ - userName: "alice", - dynamicRoles: "role1", - dynamicChannels: "docChan", - }, - userGrant{ - user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + userName: "alice", + dynamicRole: "role1", + dynamicChannel: "docChan", output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["1-0"], "updated_at":0}, @@ -534,22 +551,27 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic role and admin role grants, assert earlier sequence (dynamic role) is used", grants: []grant{ + // create user with no channels userGrant{ user: "alice", }, + // create role with channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // create another role with same channel roleGrant{ role: "role2", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign first role through sync fn docGrant{ - userName: "alice", - dynamicRoles: "role1", - dynamicChannels: "docChan", + userName: "alice", + dynamicRole: "role1", + dynamicChannel: "docChan", }, + // assign second role through admin_roles and assert sequence is from dynamic (first) role userGrant{ user: "alice", roles: []string{"role2"}, @@ -564,25 +586,30 @@ func TestGetAllChannelsByUser(t *testing.T) { { name: "channel assigned through both dynamic role and admin role grants, assert earlier sequence (admin role) is used", grants: []grant{ + // create user with no channels userGrant{ user: "alice", }, + // create role with channel roleGrant{ role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // create another role with same channel roleGrant{ role: "role2", adminChannels: map[string][]string{defaultKeyspace: {"A"}}, }, + // assign role through admin_roles userGrant{ user: "alice", - roles: []string{"role2"}, + roles: []string{"role1"}, }, + // assign other role through sync fn and assert earlier sequences are returned docGrant{ - userName: "alice", - dynamicRoles: "role1", - dynamicChannels: "docChan", + userName: "alice", + dynamicRole: "role2", + dynamicChannel: "docChan", output: ` {"all_channels":{"_default._default": { "A": { "entries" : ["4-0"], "updated_at":0}, @@ -604,7 +631,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dbConfig.Sync = base.StringPtr(`function(doc) {channel(doc.channel); access(doc.user, doc.channel); role(doc.user, doc.role);}`) rt.CreateDatabase("db", dbConfig) - // create user with adminChannels1 + // iterate and execute grants in each test case for i, grant := range test.grants { t.Logf("Processing grant %d", i+1) grant.request(rt) @@ -633,6 +660,7 @@ func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { } assert.Equal(t, expectedKeyspaces, rt.GetKeyspaces()) + // add single named collection newCollection := base.ScopeAndCollectionName{Scope: base.DefaultScope, Collection: t.Name()} require.NoError(t, rt.TestBucket.CreateDataStore(base.TestCtx(t), newCollection)) defer func() { @@ -664,7 +692,7 @@ func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { } grant.request(rt) - // check single named collection is handled + // add channel to single named collection and assert its handled grant = userGrant{ user: "alice", adminChannels: map[string][]string{defaultKeyspace: {"A"}, newCollection.String(): {"D"}}, @@ -730,7 +758,7 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { } grant.request(rt) - // check single named collection is handled + // add channel to collection with no channels and assert multi collection is handled grant = userGrant{ user: "alice", adminChannels: map[string][]string{keyspace2: {"A"}, keyspace1: {"D"}}, @@ -836,10 +864,10 @@ func TestGetAllChannelsByUserDeletedRole(t *testing.T) { roleGrant.request(rt) userGrant.request(rt) + // Delete role and assert its channels no longer appear in response resp := rt.SendAdminRequest("DELETE", "/db/_role/role1", ``) RequireStatus(t, resp, http.StatusOK) - // Delete role and assert its channels no longer appear in response userGrant.output = `{}` userGrant.roles = []string{} userGrant.request(rt) @@ -856,6 +884,7 @@ func TestGetAllChannelsByUserNonexistentAndDeletedUser(t *testing.T) { dbConfig := rt.NewDbConfig() dbConfig.Scopes = nil rt.CreateDatabase("db", dbConfig) + // assert the endpoint returns 404 when user is not found resp := rt.SendDiagnosticRequest("GET", "/db/_user/user1/_all_channels", ``) RequireStatus(t, resp, http.StatusNotFound) From ad7402d99e9dd63f6370e8eb339a021d8f78f851 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Thu, 16 May 2024 14:55:48 +0100 Subject: [PATCH 23/26] Add goimports --- rest/diagnostic_api.go | 1 + rest/diagnostic_api_test.go | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index 03c00bb101..dd9f4a54e6 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -10,6 +10,7 @@ package rest import ( "fmt" + "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" channels "github.com/couchbase/sync_gateway/channels" diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 295959372b..86488f1314 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -11,13 +11,14 @@ package rest import ( "encoding/json" "fmt" + "net/http" + "strings" + "testing" + "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "net/http" - "strings" - "testing" ) type grant interface { From 2be2762d689de5529e38c4ba343368eb8408ef1a Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Fri, 17 May 2024 15:23:56 +0100 Subject: [PATCH 24/26] Address most comments except templateResource comment --- docs/api/components/responses.yaml | 2 +- docs/api/components/schemas.yaml | 4 +- rest/diagnostic_api.go | 19 ++++-- rest/diagnostic_api_test.go | 106 ++++++++++------------------- 4 files changed, 53 insertions(+), 78 deletions(-) diff --git a/docs/api/components/responses.yaml b/docs/api/components/responses.yaml index 21190b554a..086cb762e6 100644 --- a/docs/api/components/responses.yaml +++ b/docs/api/components/responses.yaml @@ -185,4 +185,4 @@ All_user_channels_response: content: application/json: schema: - $ref: ./schemas.yaml#/All_user_channels_response + $ref: ./schemas.yaml#/all_user_channels diff --git a/docs/api/components/schemas.yaml b/docs/api/components/schemas.yaml index 3ae8bf971a..4a179d6da4 100644 --- a/docs/api/components/schemas.yaml +++ b/docs/api/components/schemas.yaml @@ -2512,7 +2512,7 @@ CollectionNames: - Starting - Stopping - Resyncing -All_user_channels_response: +all_user_channels: description: |- All user channels split by how they were assigned to the user and by keyspace. type: object @@ -2533,7 +2533,7 @@ channelEntry: properties: entries: type: array - description: Start sequence to end sequence + description: Start sequence to end sequence. If the channel is currently granted, the end sequence will be zero. updated_at: type: integer description: Unix timestamp of last update diff --git a/rest/diagnostic_api.go b/rest/diagnostic_api.go index dd9f4a54e6..5065657b1a 100644 --- a/rest/diagnostic_api.go +++ b/rest/diagnostic_api.go @@ -18,7 +18,10 @@ import ( ) type allChannels struct { - Channels map[string]map[string]auth.GrantHistory `json:"all_channels,omitempty"` + Channels map[string]map[string]channelHistory `json:"all_channels,omitempty"` +} +type channelHistory struct { + Entries []auth.GrantHistorySequencePair `json:"entries"` // Entry for a specific grant period } func (h *handler) handleGetAllChannels() error { @@ -31,7 +34,7 @@ func (h *handler) handleGetAllChannels() error { return kNotFoundError } - resp := make(map[string]map[string]auth.GrantHistory) + resp := make(map[string]map[string]channelHistory) // handles deleted collections, default/ single named collection for _, dsName := range h.db.DataStoreNames() { @@ -40,17 +43,18 @@ func (h *handler) handleGetAllChannels() error { currentChannels := user.InheritedCollectionChannels(dsName.ScopeName(), dsName.CollectionName()) chanHistory := user.CollectionChannelHistory(dsName.ScopeName(), dsName.CollectionName()) // If no channels aside from public and no channels in history, don't make a key for this keyspace - if len(currentChannels) > 1 || len(chanHistory) != 0 { - resp[keyspace] = make(map[string]auth.GrantHistory) + if len(currentChannels) == 1 && len(chanHistory) == 0 { + continue } + resp[keyspace] = make(map[string]channelHistory) for chanName, chanEntry := range currentChannels { if chanName == channels.DocumentStarChannel { continue } - resp[keyspace][chanName] = auth.GrantHistory{Entries: []auth.GrantHistorySequencePair{{StartSeq: chanEntry.Sequence, EndSeq: 0}}} + resp[keyspace][chanName] = channelHistory{Entries: []auth.GrantHistorySequencePair{{StartSeq: chanEntry.Sequence, EndSeq: 0}}} } for chanName, chanEntry := range chanHistory { - chanHistoryEntry := auth.GrantHistory{Entries: chanEntry.Entries, UpdatedAt: chanEntry.UpdatedAt} + chanHistoryEntry := channelHistory{Entries: chanEntry.Entries} // if channel is also in history, append current entry to history entries if _, chanCurrentlyAssigned := resp[keyspace][chanName]; chanCurrentlyAssigned { var newEntries []auth.GrantHistorySequencePair @@ -64,6 +68,9 @@ func (h *handler) handleGetAllChannels() error { allChannels := allChannels{Channels: resp} bytes, err := base.JSONMarshal(allChannels) + if err != nil { + return err + } h.writeRawJSON(bytes) return err } diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 86488f1314..7f6f8759b7 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -9,7 +9,6 @@ package rest import ( - "encoding/json" "fmt" "net/http" "strings" @@ -26,33 +25,13 @@ type grant interface { } const defaultKeyspace = "_default._default" -const fakeUpdatedTime = 1234 - -// convertUpdatedTimeToConstant replaces the updated_at field in the response with a constant value to be able to diff -func convertUpdatedTimeToConstant(t testing.TB, output []byte) string { - var channelMap allChannels - err := json.Unmarshal(output, &channelMap) - require.NoError(t, err) - for keyspace, channels := range channelMap.Channels { - for channelName, grant := range channels { - if grant.UpdatedAt == 0 { - continue - } - grant.UpdatedAt = fakeUpdatedTime - channelMap.Channels[keyspace][channelName] = grant - } - } - - fmt.Printf("Converted response: %s\n", string(base.MustJSONMarshal(t, channelMap))) - return string(base.MustJSONMarshal(t, channelMap)) -} func compareAllChannelsOutput(rt *RestTester, username string, expectedOutput string) { response := rt.SendDiagnosticRequest(http.MethodGet, "/{{.db}}/_user/"+username+"/_all_channels", ``) RequireStatus(rt.TB, response, http.StatusOK) rt.TB.Logf("All channels response: %s", response.BodyString()) - require.JSONEq(rt.TB, expectedOutput, convertUpdatedTimeToConstant(rt.TB, response.BodyBytes())) + require.JSONEq(rt.TB, expectedOutput, response.BodyString()) } @@ -175,9 +154,9 @@ func TestGetAllChannelsByUser(t *testing.T) { }, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0}, - "B": { "entries" : ["1-0"], "updated_at":0}, - "C": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"]}, + "B": { "entries" : ["1-0"]}, + "C": { "entries" : ["1-0"]} }}}`, }}}, { @@ -191,7 +170,7 @@ func TestGetAllChannelsByUser(t *testing.T) { }, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"]} }}}`, }, // grant 2 @@ -200,7 +179,7 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"!"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-2"], "updated_at":1234} + "A": { "entries" : ["1-2"]} }}}`, }, // grant 2 @@ -209,7 +188,7 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-2", "3-0"], "updated_at":1234} + "A": { "entries" : ["1-2", "3-0"]} }}}`, }, }, @@ -333,7 +312,7 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"], "updated_at":1234} + "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"]} }}}`, }, }, @@ -351,8 +330,8 @@ func TestGetAllChannelsByUser(t *testing.T) { roles: []string{"role1"}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["2-0"], "updated_at":0}, - "B": { "entries" : ["2-0"], "updated_at":0} + "A": { "entries" : ["2-0"]}, + "B": { "entries" : ["2-0"]} }}}`, }, }, @@ -368,7 +347,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicChannel: "A", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["2-0"], "updated_at":0} + "A": { "entries" : ["2-0"]} }}}`, }, }, @@ -392,9 +371,9 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicChannel: "chan1", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["3-0"], "updated_at":0}, - "B": { "entries" : ["3-0"], "updated_at":0}, - "chan1": { "entries" : ["3-0"], "updated_at":0} + "A": { "entries" : ["3-0"]}, + "B": { "entries" : ["3-0"]}, + "chan1": { "entries" : ["3-0"]} }}}`, }, }, @@ -413,7 +392,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicChannel: "A", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"]} }}}`, }, }, @@ -436,7 +415,7 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["2-0"], "updated_at":0} + "A": { "entries" : ["2-0"]} }}}`, }, }, @@ -460,7 +439,7 @@ func TestGetAllChannelsByUser(t *testing.T) { roles: []string{"role1"}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0} + "A": { "entries" : ["1-0"]} }}}`, }, }, @@ -488,7 +467,7 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["3-0"], "updated_at":0} + "A": { "entries" : ["3-0"]} }}}`, // A start sequence would be 4 if its broken }, }, @@ -517,8 +496,8 @@ func TestGetAllChannelsByUser(t *testing.T) { adminChannels: map[string][]string{defaultKeyspace: {"A"}}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["3-0"], "updated_at":0}, - "docChan": { "entries" : ["3-0"], "updated_at":0} + "A": { "entries" : ["3-0"]}, + "docChan": { "entries" : ["3-0"]} }}}`, }, }, @@ -543,8 +522,8 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicChannel: "docChan", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["1-0"], "updated_at":0}, - "docChan": { "entries" : ["3-0"], "updated_at":0} + "A": { "entries" : ["1-0"]}, + "docChan": { "entries" : ["3-0"]} }}}`, }, }, @@ -578,8 +557,8 @@ func TestGetAllChannelsByUser(t *testing.T) { roles: []string{"role2"}, output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["4-0"], "updated_at":0}, - "docChan": { "entries" : ["4-0"], "updated_at":0} + "A": { "entries" : ["4-0"]}, + "docChan": { "entries" : ["4-0"]} }}}`, }, }, @@ -613,8 +592,8 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicChannel: "docChan", output: ` {"all_channels":{"_default._default": { - "A": { "entries" : ["4-0"], "updated_at":0}, - "docChan": { "entries" : ["5-0"], "updated_at":0} + "A": { "entries" : ["4-0"]}, + "docChan": { "entries" : ["5-0"]} }}}`, }, }, @@ -685,8 +664,7 @@ func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { "all_channels":{ "%s":{ "D":{ - "entries":["1-0"], - "updated_at":0 + "entries":["1-0"] } } }}`, newCollection.String()), @@ -702,16 +680,14 @@ func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { "all_channels":{ "_default._default":{ "A":{ - "entries":["2-0"], - "updated_at":0 + "entries":["2-0"] } }, "%s":{ "D":{ "entries":[ "1-0" - ], - "updated_at":0 + ] } } } @@ -751,8 +727,7 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { "all_channels":{ "%s":{ "D":{ - "entries":["1-0"], - "updated_at":0 + "entries":["1-0"] } } }}`, keyspace1), @@ -768,16 +743,14 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { "all_channels":{ "%s":{ "A":{ - "entries":["2-0"], - "updated_at":0 + "entries":["2-0"] } }, "%s":{ "D":{ "entries":[ "1-0" - ], - "updated_at":0 + ] } } } @@ -794,14 +767,12 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { "all_channels":{ "%s":{ "A":{ - "entries":["2-3"], - "updated_at":1234 + "entries":["2-3"] } }, "%s":{ "D":{ - "entries":["1-0"], - "updated_at":0 + "entries":["1-0"] } } } @@ -825,8 +796,7 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { "D":{ "entries":[ "1-0" - ], - "updated_at":0 + ] } } } @@ -856,8 +826,7 @@ func TestGetAllChannelsByUserDeletedRole(t *testing.T) { "all_channels":{ "_default._default":{ "role1Chan":{ - "entries":["2-0"], - "updated_at":0 + "entries":["2-0"] } } }}`, @@ -899,8 +868,7 @@ func TestGetAllChannelsByUserNonexistentAndDeletedUser(t *testing.T) { "all_channels":{ "_default._default":{ "A":{ - "entries":["1-0"], - "updated_at":0 + "entries":["1-0"] } } }}`, From d102bc1face91cb0d9185bb9b0c467e958e5ca1f Mon Sep 17 00:00:00 2001 From: Tor Colvin Date: Tue, 21 May 2024 05:35:37 -0400 Subject: [PATCH 25/26] Template output for diagnostics to use for default collection or single named collection (#6836) --- rest/diagnostic_api_test.go | 319 +++++++++++++++++------------------- rest/utilities_testing.go | 17 +- 2 files changed, 166 insertions(+), 170 deletions(-) diff --git a/rest/diagnostic_api_test.go b/rest/diagnostic_api_test.go index 7f6f8759b7..ec8637d3b1 100644 --- a/rest/diagnostic_api_test.go +++ b/rest/diagnostic_api_test.go @@ -11,12 +11,10 @@ package rest import ( "fmt" "net/http" - "strings" "testing" "github.com/couchbase/sync_gateway/auth" "github.com/couchbase/sync_gateway/base" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -24,14 +22,12 @@ type grant interface { request(rt *RestTester) } -const defaultKeyspace = "_default._default" - func compareAllChannelsOutput(rt *RestTester, username string, expectedOutput string) { response := rt.SendDiagnosticRequest(http.MethodGet, "/{{.db}}/_user/"+username+"/_all_channels", ``) RequireStatus(rt.TB, response, http.StatusOK) rt.TB.Logf("All channels response: %s", response.BodyString()) - require.JSONEq(rt.TB, expectedOutput, response.BodyString()) + require.JSONEq(rt.TB, rt.mustTemplateResource(expectedOutput), response.BodyString()) } @@ -42,7 +38,7 @@ type userGrant struct { output string } -func (g *userGrant) getUserPayload(t testing.TB) string { +func (g *userGrant) getUserPayload(rt *RestTester) string { config := auth.PrincipalConfig{ Name: base.StringPtr(g.user), Password: base.StringPtr(RestTesterDefaultUserPassword), @@ -52,19 +48,26 @@ func (g *userGrant) getUserPayload(t testing.TB) string { } for keyspace, chans := range g.adminChannels { - scopeName, collectionName := strings.Split(keyspace, ".")[0], strings.Split(keyspace, ".")[1] - if base.IsDefaultCollection(scopeName, collectionName) { + _, scope, collection, err := ParseKeyspace(rt.mustTemplateResource(keyspace)) + require.NoError(rt.TB, err) + if scope == nil && collection == nil { + config.ExplicitChannels = base.SetFromArray(chans) + continue + } + require.NotNil(rt.TB, scope, "Could not find scope from keyspace %s", keyspace) + require.NotNil(rt.TB, collection, "Could not find collection from keyspace %s", keyspace) + if base.IsDefaultCollection(*scope, *collection) { config.ExplicitChannels = base.SetFromArray(chans) } else { - config.SetExplicitChannels(scopeName, collectionName, chans...) + config.SetExplicitChannels(*scope, *collection, chans...) } } - return string(base.MustJSONMarshal(t, config)) + return string(base.MustJSONMarshal(rt.TB, config)) } func (g userGrant) request(rt *RestTester) { - payload := g.getUserPayload(rt.TB) + payload := g.getUserPayload(rt) rt.TB.Logf("Issuing admin grant: %+v", payload) response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/_user/"+g.user, payload) if response.Code != http.StatusCreated && response.Code != http.StatusOK { @@ -80,23 +83,30 @@ type roleGrant struct { adminChannels map[string][]string } -func (g roleGrant) getPayload(t testing.TB) string { +func (g roleGrant) getPayload(rt *RestTester) string { config := auth.PrincipalConfig{ Password: base.StringPtr(RestTesterDefaultUserPassword), } for keyspace, chans := range g.adminChannels { - scopeName, collectionName := strings.Split(keyspace, ".")[0], strings.Split(keyspace, ".")[1] - if base.IsDefaultCollection(scopeName, collectionName) { + _, scope, collection, err := ParseKeyspace(rt.mustTemplateResource(keyspace)) + require.NoError(rt.TB, err) + if scope == nil && collection == nil { + config.ExplicitChannels = base.SetFromArray(chans) + continue + } + require.NotNil(rt.TB, scope, "Could not find scope from keyspace %s", keyspace) + require.NotNil(rt.TB, collection, "Could not find collection from keyspace %s", keyspace) + if base.IsDefaultCollection(*scope, *collection) { config.ExplicitChannels = base.SetFromArray(chans) } else { - config.SetExplicitChannels(scopeName, collectionName, chans...) + config.SetExplicitChannels(*scope, *collection, chans...) } } - return string(base.MustJSONMarshal(t, config)) + return string(base.MustJSONMarshal(rt.TB, config)) } func (g roleGrant) request(rt *RestTester) { - payload := g.getPayload(rt.TB) + payload := g.getPayload(rt) rt.TB.Logf("Issuing admin grant: %+v", payload) response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/_role/"+g.role, payload) if response.Code != http.StatusCreated && response.Code != http.StatusOK { @@ -111,7 +121,7 @@ type docGrant struct { output string } -func (g docGrant) getPayload(t testing.TB) string { +func (g docGrant) getPayload() string { role := fmt.Sprintf(`"role":"role:%s",`, g.dynamicRole) if g.dynamicRole == "" { role = "" @@ -126,11 +136,11 @@ func (g docGrant) getPayload(t testing.TB) string { } func (g docGrant) request(rt *RestTester) { - payload := g.getPayload(rt.TB) + payload := g.getPayload() rt.TB.Logf("Issuing dynamic grant: %+v", payload) - response := rt.SendAdminRequest(http.MethodPut, "/{{.db}}/doc", payload) + response := rt.SendAdminRequest(http.MethodPut, "/{{.keyspace}}/doc", payload) if response.Code != http.StatusCreated && response.Code != http.StatusOK { - rt.TB.Fatalf("Expected 200 or 201 exit code") + rt.TB.Fatalf("Expected 200 or 201 exit code, got %d, output: %s", response.Code, response.Body.String()) } if g.output != "" { compareAllChannelsOutput(rt, g.userName, g.output) @@ -150,10 +160,10 @@ func TestGetAllChannelsByUser(t *testing.T) { userGrant{ user: "alice", adminChannels: map[string][]string{ - defaultKeyspace: {"A", "B", "C"}, + "{{.keyspace}}": {"A", "B", "C"}, }, output: ` -{"all_channels":{"_default._default": { +{"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-0"]}, "B": { "entries" : ["1-0"]}, "C": { "entries" : ["1-0"]} @@ -166,28 +176,28 @@ func TestGetAllChannelsByUser(t *testing.T) { userGrant{ user: "alice", adminChannels: map[string][]string{ - defaultKeyspace: {"A"}, + "{{.keyspace}}": {"A"}, }, output: ` -{"all_channels":{"_default._default": { +{"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-0"]} }}}`, }, // grant 2 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, output: ` -{"all_channels":{"_default._default": { +{"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-2"]} }}}`, }, // grant 2 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` -{"all_channels":{"_default._default": { +{"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-2", "3-0"]} }}}`, }, @@ -199,119 +209,119 @@ func TestGetAllChannelsByUser(t *testing.T) { // grant 1 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 2 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 3 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 4 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 5 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 6 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 7 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 8 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 9 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 10 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 11 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 12 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 13 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 14 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 15 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 16 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 17 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 18 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 19 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 20 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 19 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // grant 20 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"!"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"!"}}, }, // grant 23 userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-4", "5-6", "7-8", "9-10", "11-12","13-14","15-16","17-18","19-20","21-22","23-0"]} }}}`, }, @@ -323,13 +333,13 @@ func TestGetAllChannelsByUser(t *testing.T) { // grant 1 roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A", "B"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A", "B"}}, }, userGrant{ user: "alice", roles: []string{"role1"}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["2-0"]}, "B": { "entries" : ["2-0"]} }}}`, @@ -346,7 +356,7 @@ func TestGetAllChannelsByUser(t *testing.T) { userName: "alice", dynamicChannel: "A", output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["2-0"]} }}}`, }, @@ -362,7 +372,7 @@ func TestGetAllChannelsByUser(t *testing.T) { // create role with channels roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A", "B"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A", "B"}}, }, // assign role through the sync fn and check output docGrant{ @@ -370,7 +380,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicRole: "role1", dynamicChannel: "chan1", output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["3-0"]}, "B": { "entries" : ["3-0"]}, "chan1": { "entries" : ["3-0"]} @@ -384,14 +394,14 @@ func TestGetAllChannelsByUser(t *testing.T) { // create user and assign channels through admin_channels userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign channels through sync fn and assert on sequences docGrant{ userName: "alice", dynamicChannel: "A", output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-0"]} }}}`, }, @@ -412,9 +422,9 @@ func TestGetAllChannelsByUser(t *testing.T) { // assign same channels through admin_channels and assert on sequences userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["2-0"]} }}}`, }, @@ -426,19 +436,19 @@ func TestGetAllChannelsByUser(t *testing.T) { // create user and assign channel through admin_channels userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // create role with same channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign role through admin_roles and assert on sequences userGrant{ user: "alice", roles: []string{"role1"}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-0"]} }}}`, }, @@ -454,7 +464,7 @@ func TestGetAllChannelsByUser(t *testing.T) { // create role with channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign role through admin_roles userGrant{ @@ -464,9 +474,9 @@ func TestGetAllChannelsByUser(t *testing.T) { // assign role channel through admin_channels and assert on sequences userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["3-0"]} }}}`, // A start sequence would be 4 if its broken }, @@ -482,7 +492,7 @@ func TestGetAllChannelsByUser(t *testing.T) { // create role with channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // create doc and assign role through sync fn docGrant{ @@ -493,9 +503,9 @@ func TestGetAllChannelsByUser(t *testing.T) { // assign role cahnnel to user through admin_channels and assert on sequences userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["3-0"]}, "docChan": { "entries" : ["3-0"]} }}}`, @@ -508,12 +518,12 @@ func TestGetAllChannelsByUser(t *testing.T) { // create user with channel userGrant{ user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // create role with same channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign role to user through sync fn and assert channel sequence is from admin_channels docGrant{ @@ -521,7 +531,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicRole: "role1", dynamicChannel: "docChan", output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["1-0"]}, "docChan": { "entries" : ["3-0"]} }}}`, @@ -538,12 +548,12 @@ func TestGetAllChannelsByUser(t *testing.T) { // create role with channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // create another role with same channel roleGrant{ role: "role2", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign first role through sync fn docGrant{ @@ -556,7 +566,7 @@ func TestGetAllChannelsByUser(t *testing.T) { user: "alice", roles: []string{"role2"}, output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["4-0"]}, "docChan": { "entries" : ["4-0"]} }}}`, @@ -573,12 +583,12 @@ func TestGetAllChannelsByUser(t *testing.T) { // create role with channel roleGrant{ role: "role1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // create another role with same channel roleGrant{ role: "role2", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, }, // assign role through admin_roles userGrant{ @@ -591,7 +601,7 @@ func TestGetAllChannelsByUser(t *testing.T) { dynamicRole: "role2", dynamicChannel: "docChan", output: ` - {"all_channels":{"_default._default": { + {"all_channels":{"{{.scopeAndCollection}}": { "A": { "entries" : ["4-0"]}, "docChan": { "entries" : ["5-0"]} }}}`, @@ -603,13 +613,11 @@ func TestGetAllChannelsByUser(t *testing.T) { t.Run(test.name, func(t *testing.T) { rt := NewRestTester(t, &RestTesterConfig{ PersistentConfig: true, + SyncFn: `function(doc) {channel(doc.channel); access(doc.user, doc.channel); role(doc.user, doc.role);}`, }) defer rt.Close() - dbConfig := rt.NewDbConfig() - dbConfig.Scopes = nil - dbConfig.Sync = base.StringPtr(`function(doc) {channel(doc.channel); access(doc.user, doc.channel); role(doc.user, doc.role);}`) - rt.CreateDatabase("db", dbConfig) + RequireStatus(t, rt.CreateDatabase("db", rt.NewDbConfig()), http.StatusCreated) // iterate and execute grants in each test case for i, grant := range test.grants { @@ -624,66 +632,59 @@ func TestGetAllChannelsByUser(t *testing.T) { func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { base.TestRequiresCollections(t) - rt := NewRestTesterMultipleCollections(t, &RestTesterConfig{PersistentConfig: true}, 1) + bucket := base.GetTestBucket(t) + rt := NewRestTesterMultipleCollections(t, &RestTesterConfig{PersistentConfig: true, CustomTestBucket: bucket}, 1) defer rt.Close() - const dbName = "db" - - // implicit default scope/collection - dbConfig := rt.NewDbConfig() - dbConfig.Scopes = nil - resp := rt.CreateDatabase(dbName, dbConfig) - RequireStatus(t, resp, http.StatusCreated) - - expectedKeyspaces := []string{ - dbName, - } - assert.Equal(t, expectedKeyspaces, rt.GetKeyspaces()) - // add single named collection newCollection := base.ScopeAndCollectionName{Scope: base.DefaultScope, Collection: t.Name()} - require.NoError(t, rt.TestBucket.CreateDataStore(base.TestCtx(t), newCollection)) + require.NoError(t, bucket.CreateDataStore(base.TestCtx(t), newCollection)) defer func() { require.NoError(t, rt.TestBucket.DropDataStore(newCollection)) }() - resp = rt.UpsertDbConfig(dbName, DbConfig{Scopes: ScopesConfig{ + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = ScopesConfig{ base.DefaultScope: {Collections: CollectionsConfig{ base.DefaultCollection: {}, newCollection.CollectionName(): {}, }}, - }}) - RequireStatus(t, resp, http.StatusCreated) + } + RequireStatus(t, rt.CreateDatabase("db", dbConfig), http.StatusCreated) // Test that the keyspace with no channels assigned does not have a key in the response grant := userGrant{ - user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {}, newCollection.String(): {"D"}}, - output: fmt.Sprintf(` + user: "alice", + adminChannels: map[string][]string{ + "{{.keyspace1}}": {}, + "{{.keyspace2}}": {"D"}}, + output: ` { "all_channels":{ - "%s":{ + "{{.scopeAndCollection2}}":{ "D":{ "entries":["1-0"] } } - }}`, newCollection.String()), + }}`, } grant.request(rt) // add channel to single named collection and assert its handled grant = userGrant{ - user: "alice", - adminChannels: map[string][]string{defaultKeyspace: {"A"}, newCollection.String(): {"D"}}, - output: fmt.Sprintf(` + user: "alice", + adminChannels: map[string][]string{ + "{{.keyspace1}}": {"A"}, + }, + output: ` { "all_channels":{ - "_default._default":{ + "{{.scopeAndCollection1}}":{ "A":{ "entries":["2-0"] } }, - "%s":{ + "{{.scopeAndCollection2}}":{ "D":{ "entries":[ "1-0" @@ -691,7 +692,7 @@ func TestGetAllChannelsByUserWithSingleNamedCollection(t *testing.T) { } } } -}`, newCollection.String()), +}`, } grant.request(rt) @@ -703,50 +704,39 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { rt := NewRestTesterMultipleCollections(t, &RestTesterConfig{PersistentConfig: true}, 2) defer rt.Close() - dbName := "db" - tb := base.GetTestBucket(t) - defer tb.Close(rt.Context()) - scopesConfig := GetCollectionsConfig(t, tb, 2) - - dbConfig := makeDbConfig(tb.GetName(), dbName, scopesConfig) - rt.CreateDatabase("db", dbConfig) - - scopeName := rt.GetDbCollections()[0].ScopeName - collection1Name := rt.GetDbCollections()[0].Name - collection2Name := rt.GetDbCollections()[1].Name - scopesConfig[scopeName].Collections[collection1Name] = &CollectionConfig{} - keyspace1 := scopeName + "." + collection1Name - keyspace2 := scopeName + "." + collection2Name + RequireStatus(t, rt.CreateDatabase("db", rt.NewDbConfig()), http.StatusCreated) // Test that the keyspace with no channels assigned does not have a key in the response grant := userGrant{ user: "alice", - adminChannels: map[string][]string{keyspace1: {"D"}}, - output: fmt.Sprintf(` + adminChannels: map[string][]string{"{{.keyspace1}}": {"D"}}, + output: ` { "all_channels":{ - "%s":{ + "{{.scopeAndCollection1}}":{ "D":{ "entries":["1-0"] } } - }}`, keyspace1), + }}`, } grant.request(rt) // add channel to collection with no channels and assert multi collection is handled grant = userGrant{ - user: "alice", - adminChannels: map[string][]string{keyspace2: {"A"}, keyspace1: {"D"}}, - output: fmt.Sprintf(` + user: "alice", + adminChannels: map[string][]string{ + "{{.keyspace2}}": {"A"}, + }, + output: ` { "all_channels":{ - "%s":{ + "{{.scopeAndCollection2}}":{ "A":{ "entries":["2-0"] } }, - "%s":{ + "{{.scopeAndCollection1}}":{ "D":{ "entries":[ "1-0" @@ -754,45 +744,48 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { } } } - }`, keyspace2, keyspace1), + }`, } grant.request(rt) // check removed channel in keyspace2 is in history before deleting collection 2 grant = userGrant{ - user: "alice", - adminChannels: map[string][]string{keyspace1: {"D"}, keyspace2: {"!"}}, - output: fmt.Sprintf(` + user: "alice", + adminChannels: map[string][]string{ + "{{.keyspace1}}": {"D"}, + "{{.keyspace2}}": {"!"}, + }, + output: ` { "all_channels":{ - "%s":{ + "{{.scopeAndCollection2}}":{ "A":{ "entries":["2-3"] } }, - "%s":{ + "{{.scopeAndCollection1}}":{ "D":{ "entries":["1-0"] } } } - }`, keyspace2, keyspace1), + }`, } grant.request(rt) // delete collection 2 - scopesConfig = GetCollectionsConfig(t, tb, 1) - dbConfig = makeDbConfig(tb.GetName(), dbName, scopesConfig) - rt.UpsertDbConfig("db", dbConfig) + dbConfig := rt.NewDbConfig() + dbConfig.Scopes = GetCollectionsConfig(t, rt.TestBucket, 1) + RequireStatus(t, rt.UpsertDbConfig("db", dbConfig), http.StatusCreated) // check deleted collection is not there grant = userGrant{ user: "alice", - adminChannels: map[string][]string{keyspace1: {"D"}}, - output: fmt.Sprintf(` + adminChannels: map[string][]string{"{{.keyspace}}": {"D"}}, + output: ` { "all_channels":{ - "%s":{ + "{{.scopeAndCollection}}":{ "D":{ "entries":[ "1-0" @@ -800,38 +793,32 @@ func TestGetAllChannelsByUserWithMultiCollections(t *testing.T) { } } } - }`, keyspace1), + }`, } grant.request(rt) } func TestGetAllChannelsByUserDeletedRole(t *testing.T) { - rt := NewRestTester(t, &RestTesterConfig{ - PersistentConfig: true, - }) + rt := NewRestTesterPersistentConfig(t) defer rt.Close() - dbConfig := rt.NewDbConfig() - dbConfig.Scopes = nil - rt.CreateDatabase("db", dbConfig) - // Create role with 1 channel and assign it to user - roleGrant := roleGrant{role: "role1", adminChannels: map[string][]string{defaultKeyspace: {"role1Chan"}}} + roleGrant := roleGrant{role: "role1", adminChannels: map[string][]string{"{{.keyspace}}": {"role1Chan"}}} + roleGrant.request(rt) userGrant := userGrant{ user: "alice", roles: []string{"role1"}, output: ` { "all_channels":{ - "_default._default":{ + "{{.scopeAndCollection}}":{ "role1Chan":{ "entries":["2-0"] } } }}`, } - roleGrant.request(rt) userGrant.request(rt) // Delete role and assert its channels no longer appear in response @@ -846,15 +833,9 @@ func TestGetAllChannelsByUserDeletedRole(t *testing.T) { func TestGetAllChannelsByUserNonexistentAndDeletedUser(t *testing.T) { - rt := NewRestTester(t, &RestTesterConfig{ - PersistentConfig: true, - }) + rt := NewRestTesterPersistentConfig(t) defer rt.Close() - dbConfig := rt.NewDbConfig() - dbConfig.Scopes = nil - rt.CreateDatabase("db", dbConfig) - // assert the endpoint returns 404 when user is not found resp := rt.SendDiagnosticRequest("GET", "/db/_user/user1/_all_channels", ``) RequireStatus(t, resp, http.StatusNotFound) @@ -862,11 +843,11 @@ func TestGetAllChannelsByUserNonexistentAndDeletedUser(t *testing.T) { // Create user and assert on response userGrant := userGrant{ user: "user1", - adminChannels: map[string][]string{defaultKeyspace: {"A"}}, + adminChannels: map[string][]string{"{{.keyspace}}": {"A"}}, output: ` { "all_channels":{ - "_default._default":{ + "{{.scopeAndCollection}}":{ "A":{ "entries":["1-0"] } diff --git a/rest/utilities_testing.go b/rest/utilities_testing.go index 0cec7f0c3d..3ef60a4820 100644 --- a/rest/utilities_testing.go +++ b/rest/utilities_testing.go @@ -656,13 +656,16 @@ func (rt *RestTester) templateResource(resource string) (string, error) { if multipleDatabases { data[fmt.Sprintf("db%dkeyspace", i+1)] = getKeyspaces(rt.TB, database)[0] } else { - data["keyspace"] = rt.GetSingleKeyspace() + keyspace := rt.GetSingleKeyspace() + data["keyspace"] = keyspace + data["scopeAndCollection"] = getScopeAndCollectionFromKeyspace(rt.TB, keyspace) } continue } for j, keyspace := range getKeyspaces(rt.TB, database) { if !multipleDatabases { data[fmt.Sprintf("keyspace%d", j+1)] = keyspace + data[fmt.Sprintf("scopeAndCollection%d", j+1)] = getScopeAndCollectionFromKeyspace(rt.TB, keyspace) } else { data[fmt.Sprintf("db%dkeyspace%d", i+1, j+1)] = keyspace } @@ -2456,6 +2459,18 @@ func getRESTKeyspace(_ testing.TB, dbName string, collection *db.DatabaseCollect return strings.Join([]string{dbName, collection.ScopeName, collection.Name}, base.ScopeCollectionSeparator) } +// getScopeAndCollectionFromKeyspace returns the scope and collection from a keyspace, e.g. /db/ -> _default._default , or /db.scope1.collection1 -> scope1.collection1 +func getScopeAndCollectionFromKeyspace(t testing.TB, keyspace string) string { + _, scope, collection, err := ParseKeyspace(keyspace) + require.NoError(t, err) + if scope == nil && collection == nil { + return strings.Join([]string{base.DefaultScope, base.DefaultCollection}, base.ScopeCollectionSeparator) + } + require.NotNil(t, scope, "Expected scope to be non-nil for %s", keyspace) + require.NotNil(t, collection, "Expected collection to be non-nil for %s", keyspace) + return strings.Join([]string{*scope, *collection}, base.ScopeCollectionSeparator) +} + // getKeyspaces returns the names of all the keyspaces on the rest tester. Currently assumes a single database. func getKeyspaces(t testing.TB, database *db.DatabaseContext) []string { var keyspaces []string From c741176dcfe821d0175f7e28396753c799725357 Mon Sep 17 00:00:00 2001 From: Mohammed Madi Date: Tue, 21 May 2024 16:09:20 +0100 Subject: [PATCH 26/26] Fix comments --- db/database.go | 2 +- docs/api/components/responses.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/db/database.go b/db/database.go index dc48c39209..253af5e581 100644 --- a/db/database.go +++ b/db/database.go @@ -2439,7 +2439,7 @@ func (dbc *DatabaseContext) InstallPrincipals(ctx context.Context, spec map[stri return nil } -// DataStoreNames returns the names of all datastore connected to this database +// DataStoreNames returns the names of all datastores connected to this database func (db *Database) DataStoreNames() base.ScopeAndCollectionNames { if db.Scopes == nil { return base.ScopeAndCollectionNames{ diff --git a/docs/api/components/responses.yaml b/docs/api/components/responses.yaml index 086cb762e6..a0162ffc75 100644 --- a/docs/api/components/responses.yaml +++ b/docs/api/components/responses.yaml @@ -181,7 +181,7 @@ DB-config-precondition-failed: error: Precondition Failed reason: Provided If-Match header does not match current config version All_user_channels_response: - description: Map of all keyspaces to all channels that the user has access to in the, and their properties. + description: Map of all keyspaces to all channels that the user has access to. content: application/json: schema: