From aa2308c5642034a0219e1ac313414ea5604befab Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Fri, 23 Aug 2024 14:33:23 -0700 Subject: [PATCH] Addressed PR feedback and fixed unit/integration tests --- cmd/api/src/api/v2/saved_queries_test.go | 1806 +---------------- cmd/api/src/api/v2/savedqueriespermissions.go | 139 ++ .../api/v2/savedqueriespermissions_test.go | 1278 ++++++++++++ cmd/api/src/database/mocks/db.go | 93 +- .../src/database/savedqueriespermissions.go | 132 ++ .../database/savedqueriespermissions_test.go | 293 +++ cmd/api/src/model/savedqueriespermissions.go | 39 + packages/go/openapi/doc/openapi.json | 87 - 8 files changed, 1912 insertions(+), 1955 deletions(-) create mode 100644 cmd/api/src/api/v2/savedqueriespermissions.go create mode 100644 cmd/api/src/api/v2/savedqueriespermissions_test.go create mode 100644 cmd/api/src/database/savedqueriespermissions.go create mode 100644 cmd/api/src/database/savedqueriespermissions_test.go create mode 100644 cmd/api/src/model/savedqueriespermissions.go diff --git a/cmd/api/src/api/v2/saved_queries_test.go b/cmd/api/src/api/v2/saved_queries_test.go index c2273af05..d7cf552b7 100644 --- a/cmd/api/src/api/v2/saved_queries_test.go +++ b/cmd/api/src/api/v2/saved_queries_test.go @@ -18,15 +18,12 @@ package v2_test import ( "context" - "database/sql" "encoding/json" "fmt" - "io" "net/http" "net/http/httptest" "net/url" "testing" - "time" uuid2 "github.com/gofrs/uuid" "github.com/gorilla/mux" @@ -885,7 +882,7 @@ func TestResources_UpdateSavedQuery_Admin_NonPublicQuery(t *testing.T) { payload := v2.CreateSavedQueryRequest{} // context owner is an admin - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -954,7 +951,7 @@ func TestResources_UpdateSavedQuery_ErrorFetchingPublicStatus(t *testing.T) { payload := v2.CreateSavedQueryRequest{} // context owner is an admin - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1082,7 +1079,7 @@ func TestResources_UpdateSavedQuery_AdminPrivateQuery_Success(t *testing.T) { payload := v2.CreateSavedQueryRequest{Name: "notFoo", Query: "notBar", Description: "notBaz"} // user is an admin - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1172,7 +1169,7 @@ func TestResources_UpdateSavedQuery_AdminPublicQuery_Success(t *testing.T) { payload := v2.CreateSavedQueryRequest{Name: "notFoo", Query: "notBar", Description: "notBaz"} // context owner is an admin - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1286,7 +1283,7 @@ func TestResources_DeleteSavedQuery_IsPublicSavedQueryDBError(t *testing.T) { mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) mockDB.EXPECT().IsSavedQueryPublic(gomock.Any(), gomock.Any()).Return(false, fmt.Errorf("error")) - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1316,7 +1313,7 @@ func TestResources_DeleteSavedQuery_NotPublicQueryAndUserIsAdmin(t *testing.T) { mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) mockDB.EXPECT().IsSavedQueryPublic(gomock.Any(), gomock.Any()).Return(false, nil) - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1440,7 +1437,7 @@ func TestResources_DeleteSavedQuery_PublicQueryAndUserIsAdmin(t *testing.T) { mockDB.EXPECT().IsSavedQueryPublic(gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().DeleteSavedQuery(gomock.Any(), gomock.Any()).Return(nil) - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1498,7 +1495,7 @@ func createContextWithOwnerId(id uuid2.UUID) context.Context { return bhCtx.ConstructGoContext() } -func createContextWithOwnerIdAsAdmin(id uuid2.UUID) context.Context { +func createContextWithAdminOwnerId(id uuid2.UUID) context.Context { bhCtx := ctx.Context{ RequestID: "", AuthCtx: auth.Context{ @@ -1508,6 +1505,7 @@ func createContextWithOwnerIdAsAdmin(id uuid2.UUID) context.Context { }, Roles: model.Roles{{ Name: auth.RoleAdministrator, + Description: "Can manage users, clients, and application configuration", Permissions: auth.Permissions().All(), }}, }, @@ -1516,1789 +1514,3 @@ func createContextWithOwnerIdAsAdmin(id uuid2.UUID) context.Context { } return bhCtx.ConstructGoContext() } - -func TestResources_ShareSavedQueries_SavingPermissionsErrors(t *testing.T) { - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - defer mockCtrl.Finish() - - endpoint := "/api/v2/saved-queries/%s/permissions" - savedQueryId := "1" - userId, err := uuid2.NewV4() - require.Nil(t, err) - userId2, err := uuid2.NewV4() - require.Nil(t, err) - userId3, err := uuid2.NewV4() - require.Nil(t, err) - - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2, userId3}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }).Return(nil, fmt.Errorf("Error!")) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusInternalServerError, response.Code) -} - -func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { - - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - - defer mockCtrl.Finish() - - endpoint := "/api/v2/saved-queries/%s/permissions" - savedQueryId := "1" - userId, err := uuid2.NewV4() - require.Nil(t, err) - userId2, err := uuid2.NewV4() - require.Nil(t, err) - userId3, err := uuid2.NewV4() - require.Nil(t, err) - - t.Run("Query doesn't belong to user error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Query shared to self error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Cannot share query to self") - }) - - t.Run("Query set to public and shared to user(s) at same time error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2}, - Public: true, - } - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Public cannot be true while user_ids is populated") - }) - - t.Run("Shared query shared to user(s) (and user(s) that already have the query shared with them) success", func(t *testing.T) { - // Request made in order to share to a user for confirming 2nd request below - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - // Request that we actually care about passing - payload2 := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2, userId3}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) - - req2, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) - require.Nil(t, err) - req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - response2 := httptest.NewRecorder() - // Using the same router as the first request - router.ServeHTTP(response2, req2) - require.Equal(t, http.StatusCreated, response2.Code) - - bodyBytes2, err := io.ReadAll(response2.Body) - require.Nil(t, err) - - var temp2 struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes2, &temp2) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(userId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - { - SharedToUserID: database.NullUUID(userId3), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp2.Data) - }) - - t.Run("Shared query shared to user(s) success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(userId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Shared query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - QueryID: 1, - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - Public: true, - }, nil) - mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId2).Return(nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId3).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - QueryID: 1, - Public: true, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Shared query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) - - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId2).Return(nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId3).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Private query shared to user(s) success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2, userId3}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(userId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - { - SharedToUserID: database.NullUUID(userId3), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Private query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - QueryID: 1, - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - Public: true, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - QueryID: 1, - Public: true, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Private query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Public query shared to user(s) error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2, userId3}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Public query set to private error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Public query set to public error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) -} - -func TestResources_SharedSavedQueries_Admin(t *testing.T) { - - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - - defer mockCtrl.Finish() - - endpoint := "/api/v2/saved-queries/%s/permissions" - savedQueryId := "1" - adminUserId, err := uuid2.NewV4() - require.Nil(t, err) - nonAdminUserId, err := uuid2.NewV4() - require.Nil(t, err) - nonAdminUserId2, err := uuid2.NewV4() - require.Nil(t, err) - - t.Run("Query set to public and shared to user(s) at same time error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId}, - Public: true, - } - - // createContextWithOwnerIdAsAdmin - // createContextWithOwnerIdAsAdmin - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Public cannot be true while user_ids is populated") - }) - - t.Run("Admin, public query shared to user(s) incorrectly error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Public query cannot be shared to users. You must set your query to private first") - }) - - t.Run("Admin, private query set to public error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - - }) - - t.Run("Admin, private query shared to user(s) error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - - }) - - t.Run("Admin, private query set to private error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Admin, shared query set to public error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Admin, shared query shared to user(s) error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Admin, shared query set to priavte error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusForbidden, response.Code) - }) - - t.Run("Admin, public query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Admin, public query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: false, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().DeleteSavedQueryPermissionPublic(gomock.Any(), gomock.Any()).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - // Test cases where admin is making operations against their own query - t.Run("Admin owned, query shared to self error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{adminUserId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Cannot share query to self") - }) - - t.Run("Admin owned, shared query shared to user(s) (and user(s) that already have the query shared with them) success", func(t *testing.T) { - // Request made in order to share to a user for confirming 2nd request below - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - // Request that we actually care about passing - payload2 := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }, nil) - - req2, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) - require.Nil(t, err) - req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - response2 := httptest.NewRecorder() - // Using the same router as the first request - router.ServeHTTP(response2, req2) - require.Equal(t, http.StatusCreated, response2.Code) - - bodyBytes2, err := io.ReadAll(response2.Body) - require.Nil(t, err) - - var temp2 struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes2, &temp2) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(nonAdminUserId), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - { - SharedToUserID: database.NullUUID(nonAdminUserId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp2.Data) - }) - - t.Run("Admin owned, shared query shared to user(s) success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(nonAdminUserId), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Admin owned, shared query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - QueryID: 1, - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - Public: true, - }, nil) - mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }, nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), nonAdminUserId).Return(nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), nonAdminUserId2).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - QueryID: 1, - Public: true, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Admin owned, shared query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }, nil) - - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), nonAdminUserId).Return(nil) - mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), nonAdminUserId2).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Admin owned, private query shared to user(s) success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(nonAdminUserId), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - { - SharedToUserID: database.NullUUID(nonAdminUserId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Admin owned, private query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - QueryID: 1, - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - Public: true, - }, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusCreated, response.Code) - - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) - - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - QueryID: 1, - Public: true, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp.Data) - }) - - t.Run("Admin owned, private query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Admin owned, public query set to public success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Admin owned, public query shared to user(s) success", func(t *testing.T) { - // First have public query set to private - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().DeleteSavedQueryPermissionPublic(gomock.Any(), gomock.Any()).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - - // Now have private query share to users - payload2 := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: false, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }).Return([]model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId), - }, - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(nonAdminUserId2), - }, - }, nil) - - req2, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) - require.Nil(t, err) - req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - response2 := httptest.NewRecorder() - // Using the same router as the first request - router.ServeHTTP(response2, req2) - require.Equal(t, http.StatusCreated, response2.Code) - - bodyBytes2, err := io.ReadAll(response2.Body) - require.Nil(t, err) - - var temp2 struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes2, &temp2) - require.Nil(t, err) - - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) - - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - SharedToUserID: database.NullUUID(nonAdminUserId), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - { - SharedToUserID: database.NullUUID(nonAdminUserId2), - QueryID: 1, - Public: false, - BigSerial: model.BigSerial{ - ID: 0, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, - }, temp2.Data) - }) - - t.Run("Admin owned, public query set to private success", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().DeleteSavedQueryPermissionPublic(gomock.Any(), gomock.Any()).Return(nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusNoContent, response.Code) - }) - - t.Run("Admin owned, public query shared to user(s) incorrectly error", func(t *testing.T) { - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, - Public: false, - } - - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - model.SavedQueryScopeOwned: true, - model.SavedQueryScopePublic: true, - model.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) - - req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "Public query cannot be shared to users. You must set your query to private first") - }) -} diff --git a/cmd/api/src/api/v2/savedqueriespermissions.go b/cmd/api/src/api/v2/savedqueriespermissions.go new file mode 100644 index 000000000..65633a236 --- /dev/null +++ b/cmd/api/src/api/v2/savedqueriespermissions.go @@ -0,0 +1,139 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v2 + +import ( + "errors" + "net/http" + "strconv" + + "github.com/gofrs/uuid" + "github.com/gorilla/mux" + "github.com/specterops/bloodhound/src/api" + "github.com/specterops/bloodhound/src/auth" + ctx2 "github.com/specterops/bloodhound/src/ctx" + "github.com/specterops/bloodhound/src/database" + "github.com/specterops/bloodhound/src/model" +) + +type ShareSavedQueriesResponse []model.SavedQueriesPermissions + +type SavedQueryPermissionRequest struct { + UserIDs []uuid.UUID `json:"user_ids"` + Public bool `json:"public"` +} + +var ( + ErrInvalidSelfShare = errors.New("invalidSelfShare") + ErrForbidden = errors.New("forbidden") + ErrInvalidPublicShare = errors.New("inavlidPublicShare") +) + +func CanUpdateSavedQueriesPermission(user model.User, savedQueryBelongsToUser bool, createRequest SavedQueryPermissionRequest, dbSavedQueryScope database.SavedQueryScopeMap) error { + if user.Roles.Has(model.Role{Name: auth.RoleAdministrator}) { + if createRequest.Public && savedQueryBelongsToUser { + return nil + } else if len(createRequest.UserIDs) == 0 && (savedQueryBelongsToUser || dbSavedQueryScope[model.SavedQueryScopePublic]) { + return nil + } else if len(createRequest.UserIDs) > 0 && !createRequest.Public { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + return ErrInvalidPublicShare + } + if savedQueryBelongsToUser { + for _, sharedUserID := range createRequest.UserIDs { + if sharedUserID == user.ID { + return ErrInvalidSelfShare + } + } + return nil + } + } + } else if savedQueryBelongsToUser && !dbSavedQueryScope[model.SavedQueryScopePublic] { + if len(createRequest.UserIDs) > 0 && !createRequest.Public { + for _, sharedUserID := range createRequest.UserIDs { + if sharedUserID == user.ID { + return ErrInvalidSelfShare + } + } + } + return nil + } + return ErrForbidden +} + +// ShareSavedQueries allows a user to share queries between users, as well as share them publicly +func (s Resources) ShareSavedQueries(response http.ResponseWriter, request *http.Request) { + var ( + rawSavedQueryID = mux.Vars(request)[api.URIPathVariableSavedQueryID] + createRequest SavedQueryPermissionRequest + ) + + if user, isUser := auth.GetUserFromAuthCtx(ctx2.FromRequest(request).AuthCtx); !isUser { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "No associated user found", request), response) + } else if savedQueryID, err := strconv.ParseInt(rawSavedQueryID, 10, 64); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) + } else if err := api.ReadJSONRequestPayloadLimited(&createRequest, request); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if createRequest.Public && len(createRequest.UserIDs) > 0 { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Public cannot be true while user_ids is populated", request), response) + } else if savedQueryBelongsToUser, err := s.DB.SavedQueryBelongsToUser(request.Context(), user.ID, savedQueryID); errors.Is(err, database.ErrNotFound) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, "Query does not exist", request), response) + } else if err != nil { + api.HandleDatabaseError(request, response, err) + } else if dbSavedQueryScope, err := s.DB.GetScopeForSavedQuery(request.Context(), savedQueryID, user.ID); err != nil { + api.HandleDatabaseError(request, response, err) + } else if err := CanUpdateSavedQueriesPermission(user, savedQueryBelongsToUser, createRequest, dbSavedQueryScope); err != nil { + if errors.Is(err, ErrInvalidSelfShare) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Cannot share query to self", request), response) + } else if errors.Is(err, ErrInvalidPublicShare) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Public query cannot be shared to users. You must set your query to private first", request), response) + } else { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + } + } else { + // Query set to public + if createRequest.Public { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + response.WriteHeader(http.StatusNoContent) + } else { + if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), savedQueryID); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + api.WriteBasicResponse(request.Context(), ShareSavedQueriesResponse{savedPermission}, http.StatusCreated, response) + } + } + // Query set to private + } else if len(createRequest.UserIDs) == 0 { + if err := s.DB.DeleteSavedQueryPermissionsForUsers(request.Context(), savedQueryID); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + response.WriteHeader(http.StatusNoContent) + } + // Sharing a query + } else if len(createRequest.UserIDs) > 0 && !createRequest.Public { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Public query cannot be shared to users. You must set your query to private first", request), response) + } else { + if savedPermissions, err := s.DB.CreateSavedQueryPermissionsToUsers(request.Context(), savedQueryID, createRequest.UserIDs...); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + api.WriteBasicResponse(request.Context(), savedPermissions, http.StatusCreated, response) + } + } + } + } +} diff --git a/cmd/api/src/api/v2/savedqueriespermissions_test.go b/cmd/api/src/api/v2/savedqueriespermissions_test.go new file mode 100644 index 000000000..14dc44a87 --- /dev/null +++ b/cmd/api/src/api/v2/savedqueriespermissions_test.go @@ -0,0 +1,1278 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package v2_test + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "testing" + "time" + + uuid2 "github.com/gofrs/uuid" + "github.com/gorilla/mux" + "github.com/specterops/bloodhound/headers" + "github.com/specterops/bloodhound/mediatypes" + v2 "github.com/specterops/bloodhound/src/api/v2" + "github.com/specterops/bloodhound/src/auth" + "github.com/specterops/bloodhound/src/database" + "github.com/specterops/bloodhound/src/database/mocks" + "github.com/specterops/bloodhound/src/model" + "github.com/specterops/bloodhound/src/test/must" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" +) + +func TestResources_ShareSavedQueriesPermissions_CanUpdateSavedQueriesPermission(t *testing.T) { + adminUser := model.User{ + Roles: model.Roles{ + { + Name: auth.RoleAdministrator, + Permissions: model.Permissions{auth.Permissions().AuthManageSelf}, + }, + }, + } + + nonAdminUserId1, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUser1 := model.User{ + Roles: model.Roles{ + { + Name: "nonAdminUser1", + }, + }, + Unique: model.Unique{ + ID: nonAdminUserId1, + }, + } + + nonAdminUserId2, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUser2 := model.User{ + Roles: model.Roles{ + { + Name: "nonAdminUser2", + }, + }, + Unique: model.Unique{ + ID: nonAdminUserId2, + }, + } + + // Non-admin owned queries + t.Run("Non-admin owned, query doesn't belong to user error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Non-admin owned, query shared to self error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrInvalidSelfShare, err) + }) + + t.Run("Non-admin owned, shared query shared to user(s)", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, shared query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, shared query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, private query shared to user(s)", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, private query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, private query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, nil, err) + }) + + t.Run("Non-admin owned, public query shared to user(s) error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Non-admin owned, public query set to private error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Non-admin owned, public query set to public error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(nonAdminUser1, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + // Admin (non-admin owned) queries + t.Run("Admin (non-admin owned), public query shared to user(s) incorrectly error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID, nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrInvalidPublicShare, err) + }) + + t.Run("Admin (non-admin owned), private query set to public error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), private query shared to user(s) error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), private query set to private error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), shared query set to public error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), shared query shared to user(s) error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID, nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), shared query set to priavte error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrForbidden, err) + }) + + t.Run("Admin (non-admin owned), public query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin (non-admin owned), public query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, false, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + // Admin owned queries + t.Run("Admin-owned, query shared to self error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{adminUser.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrInvalidSelfShare, err) + }) + + t.Run("Admin-owned, shared query shared to user(s)", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, shared query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, shared query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, private query shared to user(s)", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID, nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, private query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, private query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, public query set to public", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, public query set to private", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Nil(t, err) + }) + + t.Run("Admin-owned, public query shared to user(s) incorrectly error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUser1.ID, nonAdminUser2.ID}, + Public: false, + } + + dbSavedQueryScope := database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + } + + err := v2.CanUpdateSavedQueriesPermission(adminUser, true, payload, dbSavedQueryScope) + require.Equal(t, v2.ErrInvalidPublicShare, err) + }) +} + +func TestResources_ShareSavedQueriesPermissions_SavingPermissionsErrors(t *testing.T) { + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/permissions" + savedQueryId := "1" + userId, err := uuid2.NewV4() + require.Nil(t, err) + userId2, err := uuid2.NewV4() + require.Nil(t, err) + userId3, err := uuid2.NewV4() + require.Nil(t, err) + + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2, userId3}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), userId2, userId3).Return(nil, fmt.Errorf("Error!")) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusInternalServerError, response.Code) +} + +func TestResources_ShareSavedQueriesPermissions_NonAdmin(t *testing.T) { + + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/permissions" + savedQueryId := "1" + userId, err := uuid2.NewV4() + require.Nil(t, err) + userId2, err := uuid2.NewV4() + require.Nil(t, err) + userId3, err := uuid2.NewV4() + require.Nil(t, err) + + t.Run("Query set to public and shared to user(s) at same time error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2}, + Public: true, + } + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + + router.ServeHTTP(response, req) + require.Equal(t, http.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), "Public cannot be true while user_ids is populated") + }) + + t.Run("Shared query shared to user(s) (and user(s) that already have query shared with them) success", func(t *testing.T) { + // Request made in order to share to a user for confirming 2nd request below + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), userId2).Return([]model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) + + // Request that we actually care about passing + payload2 := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2, userId3}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), userId2, userId3).Return([]model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId3), + }, + }, nil) + + req2, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) + require.Nil(t, err) + req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + response2 := httptest.NewRecorder() + // Using the same router as the first request + router.ServeHTTP(response2, req2) + require.Equal(t, http.StatusCreated, response2.Code) + + bodyBytes2, err := io.ReadAll(response2.Body) + require.Nil(t, err) + + var temp2 struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes2, &temp2) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: database.NullUUID(userId2), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + { + SharedToUserID: database.NullUUID(userId3), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp2.Data) + }) + + t.Run("Shared query shared to user(s) success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), userId2).Return([]model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) + + bodyBytes, err := io.ReadAll(response.Body) + require.Nil(t, err) + + var temp struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes, &temp) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: database.NullUUID(userId2), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp.Data) + }) + + t.Run("Shared query set to public success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) + + bodyBytes, err := io.ReadAll(response.Body) + require.Nil(t, err) + + var temp struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes, &temp) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp.Data) + }) + + t.Run("Shared query set to private success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: true, + }, nil) + mockDB.EXPECT().DeleteSavedQueryPermissionsForUsers(gomock.Any(), gomock.Any()).Return(nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + + require.Equal(t, http.StatusNoContent, response.Code) + }) + + t.Run("Private query shared to user(s) success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2, userId3}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), userId2, userId3).Return([]model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId3), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) + + bodyBytes, err := io.ReadAll(response.Body) + require.Nil(t, err) + + var temp struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes, &temp) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: database.NullUUID(userId2), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + { + SharedToUserID: database.NullUUID(userId3), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp.Data) + }) + + t.Run("Private query set to public success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) + + bodyBytes, err := io.ReadAll(response.Body) + require.Nil(t, err) + + var temp struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes, &temp) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp.Data) + }) + + t.Run("Private query set to private success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().DeleteSavedQueryPermissionsForUsers(gomock.Any(), gomock.Any()) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusNoContent, response.Code) + }) +} + +func TestResources_ShareSavedQueriesPermissions_Admin(t *testing.T) { + + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/permissions" + savedQueryId := "1" + adminUserId, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUserId, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUserId2, err := uuid2.NewV4() + require.Nil(t, err) + + t.Run("Admin, public query set to private success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().DeleteSavedQueryPermissionsForUsers(gomock.Any(), gomock.Any()).Return(nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusNoContent, response.Code) + }) + + // Test cases where admin is making operations against their own query + t.Run("Admin owned, public query set to public success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusNoContent, response.Code) + }) + + t.Run("Admin owned, public query shared to user(s) success", func(t *testing.T) { + // First have public query set to private + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().DeleteSavedQueryPermissionsForUsers(gomock.Any(), gomock.Any()).Return(nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusNoContent, response.Code) + + // Now have private query shared to users + payload2 := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsToUsers(gomock.Any(), gomock.Any(), nonAdminUserId, nonAdminUserId2).Return([]model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId2), + }, + }, nil) + + req2, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) + require.Nil(t, err) + req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + response2 := httptest.NewRecorder() + // Using the same router as the first request + router.ServeHTTP(response2, req2) + require.Equal(t, http.StatusCreated, response2.Code) + + bodyBytes2, err := io.ReadAll(response2.Body) + require.Nil(t, err) + + var temp2 struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes2, &temp2) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + SharedToUserID: database.NullUUID(nonAdminUserId), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + { + SharedToUserID: database.NullUUID(nonAdminUserId2), + QueryID: 1, + Public: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, + }, temp2.Data) + }) +} diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index b06b53716..8c23801a8 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -83,21 +83,6 @@ func (mr *MockDatabaseMockRecorder) CancelAllFileUploads(arg0 interface{}) *gomo return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelAllFileUploads", reflect.TypeOf((*MockDatabase)(nil).CancelAllFileUploads), arg0) } -// CheckUserHasPermissionToSavedQuery mocks base method. -func (m *MockDatabase) CheckUserHasPermissionToSavedQuery(arg0 context.Context, arg1 int64, arg2 uuid.UUID) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CheckUserHasPermissionToSavedQuery", arg0, arg1, arg2) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// CheckUserHasPermissionToSavedQuery indicates an expected call of CheckUserHasPermissionToSavedQuery. -func (mr *MockDatabaseMockRecorder) CheckUserHasPermissionToSavedQuery(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CheckUserHasPermissionToSavedQuery", reflect.TypeOf((*MockDatabase)(nil).CheckUserHasPermissionToSavedQuery), arg0, arg1, arg2) -} - // Close mocks base method. func (m *MockDatabase) Close(arg0 context.Context) { m.ctrl.T.Helper() @@ -348,19 +333,24 @@ func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionToUser(arg0, arg1, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionToUser", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionToUser), arg0, arg1, arg2) } -// CreateSavedQueryPermissionsBatch mocks base method. -func (m *MockDatabase) CreateSavedQueryPermissionsBatch(arg0 context.Context, arg1 []model.SavedQueriesPermissions) ([]model.SavedQueriesPermissions, error) { +// CreateSavedQueryPermissionsToUsers mocks base method. +func (m *MockDatabase) CreateSavedQueryPermissionsToUsers(arg0 context.Context, arg1 int64, arg2 ...uuid.UUID) ([]model.SavedQueriesPermissions, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSavedQueryPermissionsBatch", arg0, arg1) + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "CreateSavedQueryPermissionsToUsers", varargs...) ret0, _ := ret[0].([]model.SavedQueriesPermissions) ret1, _ := ret[1].(error) return ret0, ret1 } -// CreateSavedQueryPermissionsBatch indicates an expected call of CreateSavedQueryPermissionsBatch. -func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionsBatch(arg0, arg1 interface{}) *gomock.Call { +// CreateSavedQueryPermissionsToUsers indicates an expected call of CreateSavedQueryPermissionsToUsers. +func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionsToUsers(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionsBatch", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionsBatch), arg0, arg1) + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionsToUsers", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionsToUsers), varargs...) } // CreateUser mocks base method. @@ -561,32 +551,23 @@ func (mr *MockDatabaseMockRecorder) DeleteSavedQuery(arg0, arg1 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQuery", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQuery), arg0, arg1) } -// DeleteSavedQueryPermissionPublic mocks base method. -func (m *MockDatabase) DeleteSavedQueryPermissionPublic(arg0 context.Context, arg1 int64) error { +// DeleteSavedQueryPermissionsForUsers mocks base method. +func (m *MockDatabase) DeleteSavedQueryPermissionsForUsers(arg0 context.Context, arg1 int64, arg2 ...uuid.UUID) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionPublic", arg0, arg1) + varargs := []interface{}{arg0, arg1} + for _, a := range arg2 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionsForUsers", varargs...) ret0, _ := ret[0].(error) return ret0 } -// DeleteSavedQueryPermissionPublic indicates an expected call of DeleteSavedQueryPermissionPublic. -func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionPublic(arg0, arg1 interface{}) *gomock.Call { +// DeleteSavedQueryPermissionsForUsers indicates an expected call of DeleteSavedQueryPermissionsForUsers. +func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUsers(arg0, arg1 interface{}, arg2 ...interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionPublic", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionPublic), arg0, arg1) -} - -// DeleteSavedQueryPermissionsForUser mocks base method. -func (m *MockDatabase) DeleteSavedQueryPermissionsForUser(arg0 context.Context, arg1 int64, arg2 uuid.UUID) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionsForUser", arg0, arg1, arg2) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSavedQueryPermissionsForUser indicates an expected call of DeleteSavedQueryPermissionsForUser. -func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUser(arg0, arg1, arg2 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUser", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUser), arg0, arg1, arg2) + varargs := append([]interface{}{arg0, arg1}, arg2...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUsers", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUsers), varargs...) } // DeleteUser mocks base method. @@ -1070,21 +1051,6 @@ func (mr *MockDatabaseMockRecorder) GetPermission(arg0, arg1 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermission", reflect.TypeOf((*MockDatabase)(nil).GetPermission), arg0, arg1) } -// GetPermissionsForSavedQuery mocks base method. -func (m *MockDatabase) GetPermissionsForSavedQuery(arg0 context.Context, arg1 int64) ([]model.SavedQueriesPermissions, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetPermissionsForSavedQuery", arg0, arg1) - ret0, _ := ret[0].([]model.SavedQueriesPermissions) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// GetPermissionsForSavedQuery indicates an expected call of GetPermissionsForSavedQuery. -func (mr *MockDatabaseMockRecorder) GetPermissionsForSavedQuery(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPermissionsForSavedQuery", reflect.TypeOf((*MockDatabase)(nil).GetPermissionsForSavedQuery), arg0, arg1) -} - // GetPublicSavedQueries mocks base method. func (m *MockDatabase) GetPublicSavedQueries(arg0 context.Context) (model.SavedQueries, error) { m.ctrl.T.Helper() @@ -1338,21 +1304,6 @@ func (mr *MockDatabaseMockRecorder) IsSavedQueryPublic(arg0, arg1 interface{}) * return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSavedQueryPublic", reflect.TypeOf((*MockDatabase)(nil).IsSavedQueryPublic), arg0, arg1) } -// IsSavedQueryShared mocks base method. -func (m *MockDatabase) IsSavedQueryShared(arg0 context.Context, arg1 int64) (bool, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "IsSavedQueryShared", arg0, arg1) - ret0, _ := ret[0].(bool) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// IsSavedQueryShared indicates an expected call of IsSavedQueryShared. -func (mr *MockDatabaseMockRecorder) IsSavedQueryShared(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IsSavedQueryShared", reflect.TypeOf((*MockDatabase)(nil).IsSavedQueryShared), arg0, arg1) -} - // ListAuditLogs mocks base method. func (m *MockDatabase) ListAuditLogs(arg0 context.Context, arg1, arg2 time.Time, arg3, arg4 int, arg5 string, arg6 model.SQLFilter) (model.AuditLogs, int, error) { m.ctrl.T.Helper() diff --git a/cmd/api/src/database/savedqueriespermissions.go b/cmd/api/src/database/savedqueriespermissions.go new file mode 100644 index 000000000..72b15b4c5 --- /dev/null +++ b/cmd/api/src/database/savedqueriespermissions.go @@ -0,0 +1,132 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package database + +import ( + "context" + + "github.com/gofrs/uuid" + "github.com/specterops/bloodhound/src/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" +) + +// SavedQueriesPermissionsData methods representing the database interactions pertaining to the saved_queries_permissions model +type SavedQueriesPermissionsData interface { + CreateSavedQueryPermissionToPublic(ctx context.Context, queryID int64) (model.SavedQueriesPermissions, error) + CreateSavedQueryPermissionsToUsers(ctx context.Context, queryID int64, userIDs ...uuid.UUID) ([]model.SavedQueriesPermissions, error) + GetScopeForSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (SavedQueryScopeMap, error) + DeleteSavedQueryPermissionsForUsers(ctx context.Context, queryID int64, userIDs ...uuid.UUID) error +} + +// SavedQueryScopeMap holds the information of a saved query's scope [IE: owned, shared, public] +type SavedQueryScopeMap map[model.SavedQueryScope]bool + +// CreateSavedQueryPermissionToUser creates a new entry to the SavedQueriesPermissions table granting a provided user id to access a provided query +func (s *BloodhoundDB) CreateSavedQueryPermissionToUser(ctx context.Context, queryID int64, userID uuid.UUID) (model.SavedQueriesPermissions, error) { + permission := model.SavedQueriesPermissions{ + QueryID: queryID, + SharedToUserID: NullUUID(userID), + Public: false, + } + + return permission, CheckError(s.db.WithContext(ctx).Create(&permission)) +} + +// CreateSavedQueryPermissionToPublic creates a new entry to the SavedQueriesPermissions table granting public read permissions to all users +func (s *BloodhoundDB) CreateSavedQueryPermissionToPublic(ctx context.Context, queryID int64) (model.SavedQueriesPermissions, error) { + permission := model.SavedQueriesPermissions{ + QueryID: queryID, + Public: true, + } + + err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := CheckError(tx.Create(&permission)); err != nil { + return err + } else if err := CheckError(tx.Table("saved_queries_permissions").Where("query_id = ? AND public = false", queryID).Delete(&model.SavedQueriesPermissions{})); err != nil { + return err + } + + return nil + }) + + return permission, err +} + +// CreateSavedQueryPermissionsBatch attempts to save the given saved query permissions in batches of 100 in a transaction +func (s *BloodhoundDB) CreateSavedQueryPermissionsToUsers(ctx context.Context, queryID int64, userIDs ...uuid.UUID) ([]model.SavedQueriesPermissions, error) { + var newPermissions []model.SavedQueriesPermissions + for _, sharedUserID := range userIDs { + newPermissions = append(newPermissions, model.SavedQueriesPermissions{ + QueryID: queryID, + SharedToUserID: NullUUID(sharedUserID), + Public: false, + }) + } + + result := s.db.WithContext(ctx).Clauses(clause.OnConflict{ + DoNothing: true, + }).CreateInBatches(&newPermissions, 100) + + return newPermissions, CheckError(result) +} + +// GetScopeForSavedQuery will return a map of the possible scopes given a query id and a user id +func (s *BloodhoundDB) GetScopeForSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (SavedQueryScopeMap, error) { + scopes := SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + } + + // Check if the query was shared with the user publicly + publicCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries_permissions").Where("public = true AND query_id = ?", queryID).Count(&publicCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if publicCount > 0 { + scopes[model.SavedQueryScopePublic] = true + } + + // Check if the user owns the query + ownedCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries").Where("id = ? AND user_id = ?", queryID, userID).Count(&ownedCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if ownedCount > 0 { + scopes[model.SavedQueryScopeOwned] = true + } + + // Check if the user has had the query shared to them + sharedCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries_permissions").Where("query_id = ? AND shared_to_user_id = ?", queryID, userID).Count(&sharedCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if sharedCount > 0 { + scopes[model.SavedQueryScopeShared] = true + } + + return scopes, nil +} + +// DeleteSavedQueryPermissionsForUsers batch deletes permissions associated a query id and a list of users +// If no user ids are supplied, all records for query id are deleted +func (s *BloodhoundDB) DeleteSavedQueryPermissionsForUsers(ctx context.Context, queryID int64, userIDs ...uuid.UUID) error { + result := s.db.WithContext(ctx).Table("saved_queries_permissions").Where("query_id = ?", queryID) + if len(userIDs) > 0 { + result = result.Where("shared_to_user_id IN ?", userIDs) + } + + return CheckError(result.Delete(&model.SavedQueriesPermissions{})) +} diff --git a/cmd/api/src/database/savedqueriespermissions_test.go b/cmd/api/src/database/savedqueriespermissions_test.go new file mode 100644 index 000000000..cff929bd9 --- /dev/null +++ b/cmd/api/src/database/savedqueriespermissions_test.go @@ -0,0 +1,293 @@ +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +//go:build integration +// +build integration + +package database_test + +import ( + "context" + "testing" + + "github.com/gofrs/uuid" + "github.com/specterops/bloodhound/src/database" + "github.com/specterops/bloodhound/src/model" + "github.com/specterops/bloodhound/src/test/integration" + "github.com/stretchr/testify/require" +) + +func TestSavedQueriesPermissions_CreateSavedQueryPermissionToPublic(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: true, + model.SavedQueryScopeOwned: true, + model.SavedQueryScopeShared: false, + }, scope) +} + +func TestSavedQueriesPermissions_CreateSavedQueryPermissionsToUsers(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + user3, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: "first3.last3@example.com", + }) + require.NoError(t, err) + + user4, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: "first4.last4@example.com", + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionsToUsers(testCtx, query.ID, user2.ID, user3.ID, user4.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user2.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope) + + scope2, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user3.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope2) + + scope3, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user4.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope3) +} + +func TestSavedQueriesPermissions_CreateSavedQueryPermissionsBatchBadDataError(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal + "2", + }) + require.NoError(t, err) + + unknownUUID, _ := uuid.NewV4() + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionsToUsers(testCtx, query.ID, user2.ID, unknownUUID) + require.Error(t, err) + + // verify partial share doesn't happen + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user2.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + }, scope) + + scope2, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, unknownUUID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + }, scope2) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryPublic(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user2.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: true, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + }, scope) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryShared(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user2.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionsToUsers(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryOwned(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionsToUsers(testCtx, query.ID, user2.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: true, + model.SavedQueryScopeShared: false, + }, scope) +} + +func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUsers(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionsToUsers(testCtx, query.ID, user2.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user2.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope) + + err = dbInst.DeleteSavedQueryPermissionsForUsers(testCtx, query.ID, user2.ID) + require.NoError(t, err) + + scope2, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user2.ID) + require.NoError(t, err) + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + }, scope2) +} diff --git a/cmd/api/src/model/savedqueriespermissions.go b/cmd/api/src/model/savedqueriespermissions.go new file mode 100644 index 000000000..3252a15f1 --- /dev/null +++ b/cmd/api/src/model/savedqueriespermissions.go @@ -0,0 +1,39 @@ +// +// Copyright 2024 Specter Ops, Inc. +// +// Licensed under the Apache License, Version 2.0 +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// SPDX-License-Identifier: Apache-2.0 + +package model + +import ( + "github.com/gofrs/uuid" +) + +type SavedQueryScope string + +const ( + SavedQueryScopeOwned SavedQueryScope = "owned" + SavedQueryScopeShared SavedQueryScope = "shared" + SavedQueryScopePublic SavedQueryScope = "public" +) + +// SavedQueriesPermissions represents the database model which allows users to share saved cypher queries +type SavedQueriesPermissions struct { + SharedToUserID uuid.NullUUID `json:"shared_to_user_id"` + QueryID int64 `json:"query_id"` + Public bool `json:"public"` + + BigSerial +} diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 27de85387..08111f852 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4437,93 +4437,6 @@ } } }, - "/api/v2/saved-queries/{saved_query_id}/share": { - "parameters": [ - { - "$ref": "#/components/parameters/header.prefer" - }, - { - "name": "saved_query_id", - "description": "ID of the saved query", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "format": "int64" - } - } - ], - "put": { - "operationId": "ShareSavedQuery", - "summary": "Share a saved query or set it to public", - "description": "Shares an existing saved query or makes it public", - "tags": [ - "Cypher", - "Community", - "Enterprise" - ], - "requestBody": { - "description": "The request body for sharing a saved query", - "required": true, - "content": { - "application/json": { - "schema": { - "properties": { - "user_ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - }, - "public": { - "type": "boolean" - } - } - } - } - } - }, - "responses": { - "201": { - "description": "Created", - "content": { - "application/json": { - "schema": { - "type": "object", - "properties": { - "data": { - "type": "array", - "items": { - "$ref": "#/components/schemas/model.saved-queries-permissions" - } - } - } - } - } - } - }, - "400": { - "$ref": "#/components/responses/bad-request" - }, - "401": { - "$ref": "#/components/responses/unauthorized" - }, - "403": { - "$ref": "#/components/responses/forbidden" - }, - "404": { - "$ref": "#/components/responses/not-found" - }, - "429": { - "$ref": "#/components/responses/too-many-requests" - }, - "500": { - "$ref": "#/components/responses/internal-server-error" - } - } - } - }, "/api/v2/saved-queries/{saved_query_id}/permissions": { "parameters": [ {