From 69e521356c607b3302d2c23253b98bcf226c30e6 Mon Sep 17 00:00:00 2001 From: Mistah J <26472282+mistahj67@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:17:45 -0700 Subject: [PATCH] Addressed PR feedback, corrected yaml files and unit tests Address PR feedback and optimization changes Refactored control flow logic, added TONS of unit tests, added some integration tests, and altered some database functions Refactored logic, added unit/integration tests, handled merge conflicts Addressed previous PR feedback and adjusted unit tests Corrected openapi stuff Changed the endpoint url More openapi corrections and file name change --- cmd/api/src/api/registration/v2.go | 2 +- cmd/api/src/api/v2/saved_queries.go | 79 - .../src/api/v2/saved_queries_permissions.go | 262 +++ cmd/api/src/api/v2/saved_queries_test.go | 1991 ++++++++++++++--- cmd/api/src/database/mocks/db.go | 56 +- .../src/database/saved_queries_permissions.go | 66 +- .../saved_queries_permissions_test.go | 120 +- .../src/model/saved_queries_permissions.go | 3 +- packages/go/openapi/doc/openapi.json | 154 +- packages/go/openapi/src/openapi.yaml | 4 +- ... cypher.saved-queries.id.permissions.yaml} | 21 +- ...del.saved-queries-permissions-request.yaml | 31 - ...l => model.saved-queries-permissions.yaml} | 12 +- 13 files changed, 2189 insertions(+), 612 deletions(-) create mode 100644 cmd/api/src/api/v2/saved_queries_permissions.go rename packages/go/openapi/src/paths/{cypher.saved-queries.id.share.yaml => cypher.saved-queries.id.permissions.yaml} (78%) delete mode 100644 packages/go/openapi/src/schemas/model.saved-queries-permissions-request.yaml rename packages/go/openapi/src/schemas/{model.saved-queries-permissions-response.yaml => model.saved-queries-permissions.yaml} (84%) diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index fc0124f62..cf6f142c1 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -165,7 +165,7 @@ func NewV2API(cfg config.Configuration, resources v2.Resources, routerInst *rout routerInst.POST("/api/v2/saved-queries", resources.CreateSavedQuery).RequirePermissions(permissions.SavedQueriesWrite), routerInst.PUT(fmt.Sprintf("/api/v2/saved-queries/{%s}", api.URIPathVariableSavedQueryID), resources.UpdateSavedQuery).RequirePermissions(permissions.SavedQueriesWrite), routerInst.DELETE(fmt.Sprintf("/api/v2/saved-queries/{%s}", api.URIPathVariableSavedQueryID), resources.DeleteSavedQuery).RequirePermissions(permissions.SavedQueriesWrite), - routerInst.PUT(fmt.Sprintf("/api/v2/saved-queries/{%s}/share", api.URIPathVariableSavedQueryID), resources.ShareSavedQueries).RequirePermissions(permissions.SavedQueriesWrite), + routerInst.PUT(fmt.Sprintf("/api/v2/saved-queries/{%s}/permissions", api.URIPathVariableSavedQueryID), resources.ShareSavedQueries).RequirePermissions(permissions.SavedQueriesWrite), // Azure Entity API routerInst.GET("/api/v2/azure/{entity_type}", resources.GetAZEntity).RequirePermissions(permissions.GraphDBRead), diff --git a/cmd/api/src/api/v2/saved_queries.go b/cmd/api/src/api/v2/saved_queries.go index 7596e094d..5a42c820c 100644 --- a/cmd/api/src/api/v2/saved_queries.go +++ b/cmd/api/src/api/v2/saved_queries.go @@ -23,7 +23,6 @@ import ( "strconv" "strings" - "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" @@ -257,81 +256,3 @@ func (s Resources) DeleteSavedQuery(response http.ResponseWriter, request *http. } } - -type ShareSavedQueriesResponse []model.SavedQueriesPermissions - -type SavedQueryPermissionRequest struct { - UserIDs []uuid.UUID `json:"user_ids"` - Public bool `json:"public"` -} - -// 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.Atoi(rawSavedQueryID); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, 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 := api.ReadJSONRequestPayloadLimited(&createRequest, request); errors.Is(err, database.ErrNotFound) { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) - } else if !savedQueryBelongsToUser { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Invalid saved_query_id supplied", request), response) - } else if scopeForSavedQuery, err := s.DB.GetScopeForSavedQuery(request.Context(), int64(savedQueryID), user.ID); err != nil { - api.HandleDatabaseError(request, response, err) - } else { - // Sharing a query as public - if createRequest.Public { - if len(createRequest.UserIDs) > 0 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "SavedQueryScopePublic cannot be true while user_ids is populated", request), response) - } else if scopeForSavedQuery[database.SavedQueryScopePublic] == true { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "User cannot make a query public that's already public", request), response) - } else if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), int64(savedQueryID)); err != nil { - api.HandleDatabaseError(request, response, err) - } else { - api.WriteBasicResponse(request.Context(), ShareSavedQueriesResponse{savedPermission}, http.StatusCreated, response) - } - // Sharing a query with one or more users - } else if len(createRequest.UserIDs) > 0 { - var newlySharedUserIDs []uuid.UUID - for _, sharedUserID := range createRequest.UserIDs { - if sharedUserID == user.ID { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Cannot Share query to self", request), response) - return - } else if hasAccess, err := s.DB.CheckUserHasPermissionToSavedQuery(request.Context(), int64(savedQueryID), sharedUserID); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, "Error checking user's query permissions", request), response) - } else if hasAccess { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("User %s already has shared permission", sharedUserID), request), response) - return - } else { - newlySharedUserIDs = append(newlySharedUserIDs, sharedUserID) - } - } - - // Create permission objects for each user that we're attempting to share to - newPermissions := make([]model.SavedQueriesPermissions, len(newlySharedUserIDs)) - for i, id := range newlySharedUserIDs { - newPermissions[i] = model.SavedQueriesPermissions{ - QueryID: int64(savedQueryID), - Public: false, - SharedToUserID: database.NullUUID(id), - } - } - - // Save the permissions to the database - if savedPermissions, err := s.DB.CreateSavedQueryPermissionsBatch(request.Context(), newPermissions); err != nil { - api.HandleDatabaseError(request, response, err) - } else { - api.WriteBasicResponse(request.Context(), savedPermissions, http.StatusCreated, response) - } - - } - } -} diff --git a/cmd/api/src/api/v2/saved_queries_permissions.go b/cmd/api/src/api/v2/saved_queries_permissions.go new file mode 100644 index 000000000..94e3c176d --- /dev/null +++ b/cmd/api/src/api/v2/saved_queries_permissions.go @@ -0,0 +1,262 @@ +// 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"` +} + +// 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(), int64(savedQueryID), user.ID); err != nil { + api.HandleDatabaseError(request, response, err) + } else if isSavedQueryShared, err := s.DB.IsSavedQueryShared(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + isAdmin := user.Roles.Has(model.Role{Name: auth.RoleAdministrator}) + + if isAdmin { + // Query set to public + if createRequest.Public { + if savedQueryBelongsToUser { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + response.WriteHeader(http.StatusNoContent) + } else { + if isSavedQueryShared { + if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else if savedQueryPermissions, err := s.DB.GetPermissionsForSavedQuery(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + for _, permission := range savedQueryPermissions { + sharedToUserID := permission.SharedToUserID + + if err := s.DB.DeleteSavedQueryPermissionsForUser(request.Context(), int64(savedQueryID), sharedToUserID.UUID); err != nil { + api.HandleDatabaseError(request, response, err) + } + } + api.WriteBasicResponse(request.Context(), ShareSavedQueriesResponse{savedPermission}, http.StatusCreated, response) + } + } else { + if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + api.WriteBasicResponse(request.Context(), ShareSavedQueriesResponse{savedPermission}, http.StatusCreated, response) + } + } + } + } else { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + response.WriteHeader(http.StatusNoContent) + } else { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + } + } + // Query set to private + } else if len(createRequest.UserIDs) == 0 { + if savedQueryBelongsToUser { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + if err := s.DB.DeleteSavedQueryPermissionPublic(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + response.WriteHeader(http.StatusNoContent) + } + } else { + if isSavedQueryShared { + if savedQueryPermissions, err := s.DB.GetPermissionsForSavedQuery(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + for _, permission := range savedQueryPermissions { + sharedToUserID := permission.SharedToUserID + + if err := s.DB.DeleteSavedQueryPermissionsForUser(request.Context(), int64(savedQueryID), sharedToUserID.UUID); err != nil { + api.HandleDatabaseError(request, response, err) + } + } + response.WriteHeader(http.StatusNoContent) + } + } else { + response.WriteHeader(http.StatusNoContent) + } + } + } else { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + if err := s.DB.DeleteSavedQueryPermissionPublic(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + response.WriteHeader(http.StatusNoContent) + } + } else { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + } + } + // Sharing a query + } else if len(createRequest.UserIDs) > 0 && !createRequest.Public { + if savedQueryBelongsToUser { + 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 { + var newPermissions []model.SavedQueriesPermissions + for _, sharedUserID := range createRequest.UserIDs { + if sharedUserID != user.ID { + newPermissions = append(newPermissions, model.SavedQueriesPermissions{ + QueryID: int64(savedQueryID), + Public: false, + SharedToUserID: database.NullUUID(sharedUserID), + }) + } else { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Cannot share query to self", request), response) + return + } + } + // Save the permissions to the database + if savedPermissions, err := s.DB.CreateSavedQueryPermissionsBatch(request.Context(), newPermissions); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + api.WriteBasicResponse(request.Context(), savedPermissions, http.StatusCreated, response) + } + } + } else { + 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 { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + } + } + } + } else if !isAdmin { + if !savedQueryBelongsToUser { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + // Query set to public + } else if createRequest.Public { + if dbSavedQueryScope[model.SavedQueryScopePublic] { + response.WriteHeader(http.StatusNoContent) + } else { + if isSavedQueryShared { + if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else if savedQueryPermissions, err := s.DB.GetPermissionsForSavedQuery(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + for _, permission := range savedQueryPermissions { + sharedToUserID := permission.SharedToUserID + + if err := s.DB.DeleteSavedQueryPermissionsForUser(request.Context(), int64(savedQueryID), sharedToUserID.UUID); err != nil { + api.HandleDatabaseError(request, response, err) + } + } + api.WriteBasicResponse(request.Context(), ShareSavedQueriesResponse{savedPermission}, http.StatusCreated, response) + } + } else { + if savedPermission, err := s.DB.CreateSavedQueryPermissionToPublic(request.Context(), int64(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 dbSavedQueryScope[model.SavedQueryScopePublic] { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + } else { + if isSavedQueryShared { + if savedQueryPermissions, err := s.DB.GetPermissionsForSavedQuery(request.Context(), int64(savedQueryID)); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + for _, permission := range savedQueryPermissions { + sharedToUserID := permission.SharedToUserID + + if err := s.DB.DeleteSavedQueryPermissionsForUser(request.Context(), int64(savedQueryID), sharedToUserID.UUID); err != nil { + api.HandleDatabaseError(request, response, err) + } + } + response.WriteHeader(http.StatusNoContent) + } + } 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.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) + return + } else { + var newPermissions []model.SavedQueriesPermissions + for _, sharedUserID := range createRequest.UserIDs { + if sharedUserID != user.ID { + newPermissions = append(newPermissions, model.SavedQueriesPermissions{ + QueryID: int64(savedQueryID), + Public: false, + SharedToUserID: database.NullUUID(sharedUserID), + }) + } else { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Cannot share query to self", request), response) + return + } + } + // Save the permissions to the database + if savedPermissions, err := s.DB.CreateSavedQueryPermissionsBatch(request.Context(), newPermissions); err != nil { + api.HandleDatabaseError(request, response, err) + } else { + api.WriteBasicResponse(request.Context(), savedPermissions, http.StatusCreated, response) + } + } + } + } + } +} diff --git a/cmd/api/src/api/v2/saved_queries_test.go b/cmd/api/src/api/v2/saved_queries_test.go index 9a9fe047a..c2273af05 100644 --- a/cmd/api/src/api/v2/saved_queries_test.go +++ b/cmd/api/src/api/v2/saved_queries_test.go @@ -885,7 +885,7 @@ func TestResources_UpdateSavedQuery_Admin_NonPublicQuery(t *testing.T) { payload := v2.CreateSavedQueryRequest{} // context owner is an admin - req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -954,7 +954,7 @@ func TestResources_UpdateSavedQuery_ErrorFetchingPublicStatus(t *testing.T) { payload := v2.CreateSavedQueryRequest{} // context owner is an admin - req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1082,7 +1082,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(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1172,7 +1172,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(createContextWithAdminOwnerId(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "PUT", fmt.Sprintf(endpoint, "1"), must.MarshalJSONReader(payload)) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1286,7 +1286,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(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1316,7 +1316,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(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1440,7 +1440,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(createContextWithAdminOwnerId(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(userId), "DELETE", fmt.Sprintf(endpoint, savedQueryId), nil) require.Nil(t, err) req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) @@ -1498,7 +1498,26 @@ func createContextWithOwnerId(id uuid2.UUID) context.Context { return bhCtx.ConstructGoContext() } -func TestResources_ShareSavedQueries_Success(t *testing.T) { +func createContextWithOwnerIdAsAdmin(id uuid2.UUID) context.Context { + bhCtx := ctx.Context{ + RequestID: "", + AuthCtx: auth.Context{ + Owner: model.User{ + Unique: model.Unique{ + ID: id, + }, + Roles: model.Roles{{ + Name: auth.RoleAdministrator, + Permissions: auth.Permissions().All(), + }}, + }, + }, + Host: nil, + } + return bhCtx.ConstructGoContext() +} + +func TestResources_ShareSavedQueries_SavingPermissionsErrors(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockDB = mocks.NewMockDatabase(mockCtrl) @@ -1506,7 +1525,7 @@ func TestResources_ShareSavedQueries_Success(t *testing.T) { ) defer mockCtrl.Finish() - endpoint := "/api/v2/saved-queries/%s/share" + endpoint := "/api/v2/saved-queries/%s/permissions" savedQueryId := "1" userId, err := uuid2.NewV4() require.Nil(t, err) @@ -1522,11 +1541,11 @@ func TestResources_ShareSavedQueries_Success(t *testing.T) { mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, }, nil) - mockDB.EXPECT().CheckUserHasPermissionToSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil).Times(2) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ { QueryID: int64(1), @@ -1538,20 +1557,7 @@ func TestResources_ShareSavedQueries_Success(t *testing.T) { Public: false, SharedToUserID: database.NullUUID(userId3), }, - }).Return([]model.SavedQueriesPermissions{ - { - ID: 1, - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId2), - }, - { - ID: 2, - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) + }).Return(nil, fmt.Errorf("Error!")) req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) require.Nil(t, err) @@ -1559,363 +1565,1740 @@ func TestResources_ShareSavedQueries_Success(t *testing.T) { req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + 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{ - { - ID: 1, - SharedToUserID: database.NullUUID(userId2), - QueryID: 1, - Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - { - ID: 2, - SharedToUserID: database.NullUUID(userId3), - QueryID: 1, - Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, - }, - }, - }, - }, temp.Data) + require.Equal(t, http.StatusInternalServerError, response.Code) } -func TestResources_ShareSavedQueries_PublicSuccess(t *testing.T) { +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/share" + 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{}, - Public: true, - } + 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(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - ID: 1, - QueryID: 1, - SharedToUserID: uuid2.NullUUID{ - UUID: uuid2.UUID{}, - Valid: false, - }, - Public: true, - }, nil) + 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, 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()) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + 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) + 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) - bodyBytes, err := io.ReadAll(response.Body) - require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - var temp struct { - Data v2.ShareSavedQueriesResponse `json:"data"` - } - err = json.Unmarshal(bodyBytes, &temp) - require.Nil(t, err) + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") - require.Nil(t, err) + 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") + }) - require.Equal(t, v2.ShareSavedQueriesResponse{ - { - ID: 1, + 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, }, - QueryID: 1, - Public: true, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, + 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) -} + }, 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) -func TestResources_ShareSavedQueries_PublicAndUserIDsBothSetError(t *testing.T) { - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - defer mockCtrl.Finish() + mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId2).Return(nil) + mockDB.EXPECT().DeleteSavedQueryPermissionsForUser(gomock.Any(), gomock.Any(), userId3).Return(nil) - endpoint := "/api/v2/saved-queries/%s/share" - savedQueryId := "1" - userId, err := uuid2.NewV4() - require.Nil(t, err) - userId2, err := uuid2.NewV4() - require.Nil(t, err) + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2}, - Public: true, - } + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, - }, nil) + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) + response := httptest.NewRecorder() + router.ServeHTTP(response, req) - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + 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) - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "SavedQueryScopePublic cannot be true while user_ids is populated") -} + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) -func TestResources_ShareSavedQueries_IsAlreadyPublicError(t *testing.T) { - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - defer mockCtrl.Finish() + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - endpoint := "/api/v2/saved-queries/%s/share" - savedQueryId := "1" - userId, err := uuid2.NewV4() - require.Nil(t, err) + 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) + }) - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{}, - Public: true, - } + 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) - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: true, - database.SavedQueryScopeShared: false, - }, nil) + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) - 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()) - 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") - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusCreated, response.Code) - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), "User cannot make a query public that's already public") + 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_ShareSavedQueries_UserAlreadySharedToError(t *testing.T) { +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/share" + endpoint := "/api/v2/saved-queries/%s/permissions" savedQueryId := "1" - userId, err := uuid2.NewV4() + adminUserId, err := uuid2.NewV4() require.Nil(t, err) - userId2, err := uuid2.NewV4() + nonAdminUserId, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUserId2, err := uuid2.NewV4() require.Nil(t, err) - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2}, - Public: false, - } + 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, + } - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().CheckUserHasPermissionToSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().CheckUserHasPermissionToSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ - { - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }).Return([]model.SavedQueriesPermissions{ - { - ID: 1, - QueryID: int64(1), - Public: false, - SharedToUserID: database.NullUUID(userId3), - }, - }, nil) + // createContextWithOwnerIdAsAdmin + // createContextWithOwnerIdAsAdmin + req, err := http.NewRequestWithContext(createContextWithOwnerIdAsAdmin(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) - 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()) - 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") - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + response := httptest.NewRecorder() - response := httptest.NewRecorder() - router.ServeHTTP(response, req) - require.Equal(t, http.StatusBadRequest, response.Code) - require.Contains(t, response.Body.String(), fmt.Sprintf("User %s already has shared permission", userId2)) -} + 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()) -func TestResources_ShareSavedQueries_UserSharesToSelfError(t *testing.T) { - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - defer mockCtrl.Finish() + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - endpoint := "/api/v2/saved-queries/%s/share" - savedQueryId := "1" - userId, err := uuid2.NewV4() - require.Nil(t, err) + 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") + }) - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId}, - Public: false, - } + 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()) - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, - }, nil) + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) + 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()) - 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") - router := mux.NewRouter() - router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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()) - 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") -} + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") -func TestResources_ShareSavedQueries_SavingPermissionsErrors(t *testing.T) { - var ( - mockCtrl = gomock.NewController(t) - mockDB = mocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{DB: mockDB} - ) - defer mockCtrl.Finish() + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) - endpoint := "/api/v2/saved-queries/%s/share" - 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("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()) - payload := v2.SavedQueryPermissionRequest{ - UserIDs: []uuid2.UUID{userId2, userId3}, - Public: false, - } + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) - mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ - database.SavedQueryScopeOwned: false, - database.SavedQueryScopePublic: false, - database.SavedQueryScopeShared: false, - }, nil) - mockDB.EXPECT().CheckUserHasPermissionToSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) - mockDB.EXPECT().CheckUserHasPermissionToSavedQuery(gomock.Any(), 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!")) + 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()) - req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) - require.Nil(t, err) + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/permissions", resources.ShareSavedQueries).Methods("PUT") - req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + 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}/share", resources.ShareSavedQueries).Methods("PUT") + 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) + 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/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index ca6dd6857..b06b53716 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -561,18 +561,18 @@ func (mr *MockDatabaseMockRecorder) DeleteSavedQuery(arg0, arg1 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQuery", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQuery), arg0, arg1) } -// DeleteSavedQueryPermission mocks base method. -func (m *MockDatabase) DeleteSavedQueryPermission(arg0 context.Context, arg1 int64) error { +// DeleteSavedQueryPermissionPublic mocks base method. +func (m *MockDatabase) DeleteSavedQueryPermissionPublic(arg0 context.Context, arg1 int64) error { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSavedQueryPermission", arg0, arg1) + ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionPublic", arg0, arg1) ret0, _ := ret[0].(error) return ret0 } -// DeleteSavedQueryPermission indicates an expected call of DeleteSavedQueryPermission. -func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermission(arg0, arg1 interface{}) *gomock.Call { +// DeleteSavedQueryPermissionPublic indicates an expected call of DeleteSavedQueryPermissionPublic. +func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionPublic(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermission", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermission), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionPublic", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionPublic), arg0, arg1) } // DeleteSavedQueryPermissionsForUser mocks base method. @@ -589,20 +589,6 @@ func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUser(arg0, arg return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUser", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUser), arg0, arg1, arg2) } -// DeleteSavedQueryPermissionsForUsers mocks base method. -func (m *MockDatabase) DeleteSavedQueryPermissionsForUsers(arg0 context.Context, arg1 int64) error { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionsForUsers", arg0, arg1) - ret0, _ := ret[0].(error) - return ret0 -} - -// DeleteSavedQueryPermissionsForUsers indicates an expected call of DeleteSavedQueryPermissionsForUsers. -func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUsers(arg0, arg1 interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUsers", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUsers), arg0, arg1) -} - // DeleteUser mocks base method. func (m *MockDatabase) DeleteUser(arg0 context.Context, arg1 model.User) error { m.ctrl.T.Helper() @@ -1174,6 +1160,21 @@ func (mr *MockDatabaseMockRecorder) GetSAMLProviderUsers(arg0, arg1 interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSAMLProviderUsers", reflect.TypeOf((*MockDatabase)(nil).GetSAMLProviderUsers), arg0, arg1) } +// GetSavedQuery mocks base method. +func (m *MockDatabase) GetSavedQuery(arg0 context.Context, arg1 int64) (model.SavedQuery, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetSavedQuery", arg0, arg1) + ret0, _ := ret[0].(model.SavedQuery) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetSavedQuery indicates an expected call of GetSavedQuery. +func (mr *MockDatabaseMockRecorder) GetSavedQuery(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSavedQuery", reflect.TypeOf((*MockDatabase)(nil).GetSavedQuery), arg0, arg1) +} + // GetScopeForSavedQuery mocks base method. func (m *MockDatabase) GetScopeForSavedQuery(arg0 context.Context, arg1 int64, arg2 uuid.UUID) (database.SavedQueryScopeMap, error) { m.ctrl.T.Helper() @@ -1337,6 +1338,21 @@ 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/saved_queries_permissions.go b/cmd/api/src/database/saved_queries_permissions.go index 4498ab36b..af8dd4905 100644 --- a/cmd/api/src/database/saved_queries_permissions.go +++ b/cmd/api/src/database/saved_queries_permissions.go @@ -18,10 +18,11 @@ package database import ( "context" - "gorm.io/gorm/clause" "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 @@ -32,21 +33,13 @@ type SavedQueriesPermissionsData interface { CheckUserHasPermissionToSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (bool, error) GetPermissionsForSavedQuery(ctx context.Context, queryID int64) ([]model.SavedQueriesPermissions, error) GetScopeForSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (SavedQueryScopeMap, error) - DeleteSavedQueryPermission(ctx context.Context, permissionID int64) error + DeleteSavedQueryPermissionPublic(ctx context.Context, queryID int64) error DeleteSavedQueryPermissionsForUser(ctx context.Context, queryID int64, userID uuid.UUID) error - DeleteSavedQueryPermissionsForUsers(ctx context.Context, queryID int64) error + IsSavedQueryShared(ctx context.Context, queryID int64) (bool, error) } -type SavedQueryScope string - -const ( - SavedQueryScopeOwned SavedQueryScope = "Owned" - SavedQueryScopeShared SavedQueryScope = "Shared" - SavedQueryScopePublic SavedQueryScope = "Public" -) - // SavedQueryScopeMap holds the information of a saved query's scope [IE: owned, shared, public] -type SavedQueryScopeMap map[SavedQueryScope]bool +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) { @@ -66,7 +59,17 @@ func (s *BloodhoundDB) CreateSavedQueryPermissionToPublic(ctx context.Context, q Public: true, } - return permission, CheckError(s.db.WithContext(ctx).Create(&permission)) + 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 @@ -97,41 +100,41 @@ func (s *BloodhoundDB) GetPermissionsForSavedQuery(ctx context.Context, queryID // 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{ - SavedQueryScopePublic: false, - SavedQueryScopeOwned: false, - SavedQueryScopeShared: false, + 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, result.Error + return scopes, CheckError(result) } else if publicCount > 0 { - scopes[SavedQueryScopePublic] = true + 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, result.Error + return scopes, CheckError(result) } else if ownedCount > 0 { - scopes[SavedQueryScopeOwned] = true + 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, result.Error + return scopes, CheckError(result) } else if sharedCount > 0 { - scopes[SavedQueryScopeShared] = true + scopes[model.SavedQueryScopeShared] = true } return scopes, nil } -// DeleteSavedQueryPermission deletes the saved query permission associated with the passed in permission id -func (s *BloodhoundDB) DeleteSavedQueryPermission(ctx context.Context, permissionID int64) error { - return CheckError(s.db.WithContext(ctx).Table("saved_queries_permissions").Delete(&model.SavedQueriesPermissions{}, permissionID)) +// DeleteSavedQueryPermissionPublic deletes the saved query permission associated with the passed in query id and public = true +func (s *BloodhoundDB) DeleteSavedQueryPermissionPublic(ctx context.Context, queryID int64) error { + return CheckError(s.db.WithContext(ctx).Table("saved_queries_permissions").Where("public = true AND query_id = ?", queryID).Delete(&model.SavedQueriesPermissions{})) } // DeleteSavedQueryPermissionsForUser deletes all permissions associated with the passed in query id and user id @@ -139,7 +142,14 @@ func (s *BloodhoundDB) DeleteSavedQueryPermissionsForUser(ctx context.Context, q return CheckError(s.db.WithContext(ctx).Table("saved_queries_permissions").Where("query_id = ? AND shared_to_user_id = ?", queryID, userID).Delete(&model.SavedQueriesPermissions{})) } -// DeleteSavedQueryPermissionsForUsers deletes all permissions for a query id that are shared to users, ignoring publicly shared permissions -func (s *BloodhoundDB) DeleteSavedQueryPermissionsForUsers(ctx context.Context, queryID int64) error { - return CheckError(s.db.WithContext(ctx).Table("saved_queries_permissions").Where("query_id = ? AND public = false", queryID).Delete(&model.SavedQueriesPermissions{})) +// IsSavedQueryShared returns true or false depending on if the saved query is being shared with users +func (s *BloodhoundDB) IsSavedQueryShared(ctx context.Context, queryID int64) (bool, error) { + sharedCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries_permissions").Where("query_id = ? AND shared_to_user_id IS NOT NULL", queryID).Count(&sharedCount).Limit(1); result.Error != nil { + return false, CheckError(result) + } else if sharedCount > 0 { + return true, nil + } else { + return false, nil + } } diff --git a/cmd/api/src/database/saved_queries_permissions_test.go b/cmd/api/src/database/saved_queries_permissions_test.go index 8190c7fe4..ff39d21da 100644 --- a/cmd/api/src/database/saved_queries_permissions_test.go +++ b/cmd/api/src/database/saved_queries_permissions_test.go @@ -21,9 +21,10 @@ package database_test import ( "context" + "testing" + uuid2 "github.com/gofrs/uuid" "github.com/google/uuid" - "testing" "github.com/specterops/bloodhound/src/database" "github.com/specterops/bloodhound/src/model" @@ -32,54 +33,6 @@ import ( "github.com/stretchr/testify/require" ) -func TestSavedQueriesPermissions_SharingToUser(t *testing.T) { - var ( - testCtx = context.Background() - dbInst = integration.SetupDB(t) - ) - - user, 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, user.ID, "Test Query", "MATCH(n) RETURN n", "An example Query") - require.NoError(t, err) - - permissions, err := dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) - require.NoError(t, err) - - assert.Equal(t, database.NullUUID(user2.ID), permissions.SharedToUserID) - assert.Equal(t, false, permissions.Public) - assert.Equal(t, query.ID, permissions.QueryID) -} - -func TestSavedQueriesPermissions_SharingToGlobal(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", "MATCH(n) RETURN n", "An example Query") - require.NoError(t, err) - - permissions, err := dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) - require.NoError(t, err) - - assert.Equal(t, true, permissions.Public) - assert.Equal(t, query.ID, permissions.QueryID) -} - func TestSavedQueriesPermissions_CheckUserHasPermissionToSavedQuery(t *testing.T) { var ( testCtx = context.Background() @@ -219,9 +172,9 @@ func TestSavedQueriesPermissions_GetScopeForSavedQueryPublic(t *testing.T) { require.NoError(t, err) require.Equal(t, database.SavedQueryScopeMap{ - database.SavedQueryScopePublic: true, - database.SavedQueryScopeOwned: false, - database.SavedQueryScopeShared: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, }, scope) } @@ -251,9 +204,9 @@ func TestSavedQueriesPermissions_GetScopeForSavedQueryShared(t *testing.T) { require.NoError(t, err) require.Equal(t, database.SavedQueryScopeMap{ - database.SavedQueryScopePublic: false, - database.SavedQueryScopeOwned: false, - database.SavedQueryScopeShared: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, }, scope) } @@ -283,13 +236,13 @@ func TestSavedQueriesPermissions_GetScopeForSavedQueryOwned(t *testing.T) { require.NoError(t, err) require.Equal(t, database.SavedQueryScopeMap{ - database.SavedQueryScopePublic: false, - database.SavedQueryScopeOwned: true, - database.SavedQueryScopeShared: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: true, + model.SavedQueryScopeShared: false, }, scope) } -func TestSavedQueriesPermissions_DeleteSavedQueryPermission(t *testing.T) { +func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUser(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) @@ -300,25 +253,30 @@ func TestSavedQueriesPermissions_DeleteSavedQueryPermission(t *testing.T) { }) 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.CreateSavedQueryPermissionToPublic(testCtx, query.ID) + _, err = dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) require.NoError(t, err) - permissions, err := dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) + hasPermission, err := dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) require.NoError(t, err) - assert.Len(t, permissions, 1) + require.True(t, hasPermission) - err = dbInst.DeleteSavedQueryPermission(testCtx, query.ID) + err = dbInst.DeleteSavedQueryPermissionsForUser(testCtx, query.ID, user2.ID) require.NoError(t, err) - permissions, err = dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) + hasPermission, err = dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) require.NoError(t, err) - assert.Len(t, permissions, 0) + assert.False(t, hasPermission) } -func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUser(t *testing.T) { +func TestSavedQueriesPermissions_DeleteSavedQueryPermissionPublic(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) @@ -329,30 +287,17 @@ func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUser(t *testing.T }) 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.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) - require.NoError(t, err) - - hasPermission, err := dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) - require.NoError(t, err) - require.True(t, hasPermission) - - err = dbInst.DeleteSavedQueryPermissionsForUser(testCtx, query.ID, user2.ID) + _, err = dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) require.NoError(t, err) - hasPermission, err = dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) + err = dbInst.DeleteSavedQueryPermissionPublic(testCtx, query.ID) require.NoError(t, err) - assert.False(t, hasPermission) } -func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUsers(t *testing.T) { +func TestSavedQueriesPermissions_IsSavedQueryShared(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) @@ -374,14 +319,7 @@ func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUsers(t *testing. _, err = dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) require.NoError(t, err) - permissions, err := dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) - require.NoError(t, err) - require.Len(t, permissions, 1) - - err = dbInst.DeleteSavedQueryPermissionsForUsers(testCtx, query.ID) - require.NoError(t, err) - - permissions, err = dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) + hasPermission, err := dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) require.NoError(t, err) - assert.Len(t, permissions, 0) + require.True(t, hasPermission) } diff --git a/cmd/api/src/model/saved_queries_permissions.go b/cmd/api/src/model/saved_queries_permissions.go index 7513f8a5a..3252a15f1 100644 --- a/cmd/api/src/model/saved_queries_permissions.go +++ b/cmd/api/src/model/saved_queries_permissions.go @@ -31,10 +31,9 @@ const ( // SavedQueriesPermissions represents the database model which allows users to share saved cypher queries type SavedQueriesPermissions struct { - ID int64 `json:"id"` SharedToUserID uuid.NullUUID `json:"shared_to_user_id"` QueryID int64 `json:"query_id"` Public bool `json:"public"` - Basic + BigSerial } diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 6868771c2..27de85387 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4468,7 +4468,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/model.saved-queries-permissions-request" + "properties": { + "user_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "public": { + "type": "boolean" + } + } } } } @@ -4484,7 +4495,7 @@ "data": { "type": "array", "items": { - "$ref": "#/components/schemas/model.saved-queries-permissions-response" + "$ref": "#/components/schemas/model.saved-queries-permissions" } } } @@ -4513,6 +4524,97 @@ } } }, + "/api/v2/saved-queries/{saved_query_id}/permissions": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "name": "saved_query_id", + "description": "ID of the saved query", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int32" + } + } + ], + "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 or making it public", + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "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" + } + } + } + } + } + } + }, + "204": { + "$ref": "#/components/responses/no-content" + }, + "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/graphs/cypher": { "parameters": [ { @@ -14746,36 +14848,7 @@ } ] }, - "model.saved-queries-permissions-request": { - "allOf": [ - { - "$ref": "#/components/schemas/model.components.int64.id" - }, - { - "$ref": "#/components/schemas/model.components.timestamps" - }, - { - "type": "object", - "properties": { - "query_id": { - "type": "integer", - "format": "int64" - }, - "user_ids": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } - }, - "public": { - "type": "boolean" - } - } - } - ] - }, - "model.saved-queries-permissions-response": { + "model.saved-queries-permissions": { "allOf": [ { "$ref": "#/components/schemas/model.components.int64.id" @@ -14786,16 +14859,15 @@ { "type": "object", "properties": { - "id": { - "type": "integer", - "format": "int64" - }, "shared_to_user_id": { - "type": "array", - "items": { - "type": "string", - "format": "uuid" - } + "allOf": [ + { + "$ref": "#/components/schemas/null.uuid" + }, + { + "readOnly": true + } + ] }, "query_id": { "type": "integer", diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index c23bb978f..3b2167ff7 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -324,8 +324,8 @@ paths: $ref: './paths/cypher.saved-queries.yaml' /api/v2/saved-queries/{saved_query_id}: $ref: './paths/cypher.saved-queries.id.yaml' - /api/v2/saved-queries/{saved_query_id}/share: - $ref: './paths/cypher.saved-queries.id.share.yaml' + /api/v2/saved-queries/{saved_query_id}/permissions: + $ref: './paths/cypher.saved-queries.id.permissions.yaml' /api/v2/graphs/cypher: $ref: './paths/cypher.graphs.cypher.yaml' diff --git a/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml b/packages/go/openapi/src/paths/cypher.saved-queries.id.permissions.yaml similarity index 78% rename from packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml rename to packages/go/openapi/src/paths/cypher.saved-queries.id.permissions.yaml index 4a377743a..eb1b9568e 100644 --- a/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml +++ b/packages/go/openapi/src/paths/cypher.saved-queries.id.permissions.yaml @@ -22,7 +22,7 @@ parameters: required: true schema: type: integer - format: int64 + format: int32 put: operationId: ShareSavedQuery summary: Share a saved query or set it to public @@ -32,12 +32,21 @@ put: - Community - Enterprise requestBody: - description: The request body for sharing a saved query + description: The request body for sharing a saved query or making it public required: true content: application/json: schema: - $ref: './../schemas/model.saved-queries-permissions-request.yaml' + type: object + properties: + user_ids: + type: array + items: + type: string + format: uuid + public: + type: boolean + responses: 201: description: Created @@ -49,7 +58,9 @@ put: data: type: array items: - $ref: './../schemas/model.saved-queries-permissions-response.yaml' + $ref: './../schemas/model.saved-queries-permissions.yaml' + 204: + $ref: './../responses/no-content.yaml' 400: $ref: './../responses/bad-request.yaml' 401: @@ -61,4 +72,4 @@ put: 429: $ref: './../responses/too-many-requests.yaml' 500: - $ref: './../responses/internal-server-error.yaml' + $ref: './../responses/internal-server-error.yaml' \ No newline at end of file diff --git a/packages/go/openapi/src/schemas/model.saved-queries-permissions-request.yaml b/packages/go/openapi/src/schemas/model.saved-queries-permissions-request.yaml deleted file mode 100644 index 4874ed1b9..000000000 --- a/packages/go/openapi/src/schemas/model.saved-queries-permissions-request.yaml +++ /dev/null @@ -1,31 +0,0 @@ - # 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 - -allOf: - - $ref: './model.components.int64.id.yaml' - - $ref: './model.components.timestamps.yaml' - - type: object - properties: - query_id: - type: integer - format: int64 - user_ids: - type: array - items: - type: string - format: uuid - public: - type: boolean diff --git a/packages/go/openapi/src/schemas/model.saved-queries-permissions-response.yaml b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml similarity index 84% rename from packages/go/openapi/src/schemas/model.saved-queries-permissions-response.yaml rename to packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml index 9228b8e56..d9590ac2d 100644 --- a/packages/go/openapi/src/schemas/model.saved-queries-permissions-response.yaml +++ b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml @@ -19,16 +19,12 @@ allOf: - $ref: './model.components.timestamps.yaml' - type: object properties: - id: - type: integer - format: int64 shared_to_user_id: - type: array - items: - type: string - format: uuid + allOf: + - $ref: './null.uuid.yaml' + - readOnly: true query_id: type: integer format: int64 public: - type: boolean + type: boolean \ No newline at end of file