From f95108b8f8d0af06ca838125f23946c3ec8857da Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Mon, 19 Aug 2024 23:39:48 -0700 Subject: [PATCH 1/5] Refactored logic, added unit/integration tests, handled merge conflicts --- cmd/api/src/api/registration/v2.go | 1 + cmd/api/src/api/v2/saved_queries.go | 232 +++ cmd/api/src/api/v2/saved_queries_test.go | 1784 +++++++++++++++++ cmd/api/src/database/mocks/db.go | 78 +- .../src/database/saved_queries_permissions.go | 100 +- .../saved_queries_permissions_test.go | 270 ++- packages/go/openapi/doc/openapi.json | 123 ++ packages/go/openapi/src/openapi.yaml | 2 + .../paths/cypher.saved-queries.id.share.yaml | 72 + .../model.saved-queries-permissions.yaml | 33 + 10 files changed, 2675 insertions(+), 20 deletions(-) create mode 100644 packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml create mode 100644 packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index a04efb6b11..fc0124f620 100644 --- a/cmd/api/src/api/registration/v2.go +++ b/cmd/api/src/api/registration/v2.go @@ -165,6 +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), // 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 5a42c820c5..7a7d7418eb 100644 --- a/cmd/api/src/api/v2/saved_queries.go +++ b/cmd/api/src/api/v2/saved_queries.go @@ -23,6 +23,7 @@ import ( "strconv" "strings" + "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" @@ -256,3 +257,234 @@ 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.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 3d69dabdae..760ed502cd 100644 --- a/cmd/api/src/api/v2/saved_queries_test.go +++ b/cmd/api/src/api/v2/saved_queries_test.go @@ -18,12 +18,15 @@ package v2_test import ( "context" + "database/sql" "encoding/json" "fmt" + "io" "net/http" "net/http/httptest" "net/url" "testing" + "time" uuid2 "github.com/gofrs/uuid" "github.com/gorilla/mux" @@ -1506,6 +1509,7 @@ func createContextWithAdminOwnerId(id uuid2.UUID) context.Context { Roles: []model.Role{{ Name: auth.RoleAdministrator, Description: "Can manage users, clients, and application configuration", + Permissions: auth.Permissions().All(), }}, }, }, @@ -1513,3 +1517,1783 @@ func createContextWithAdminOwnerId(id uuid2.UUID) context.Context { } return bhCtx.ConstructGoContext() } + +func TestResources_ShareSavedQueries_SavingPermissionsErrors(t *testing.T) { + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/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) + + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2, userId3}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId3), + }, + }).Return(nil, fmt.Errorf("Error!")) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusInternalServerError, response.Code) +} + +func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { + + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/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("Query doesn't belong to user error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) + + t.Run("Query shared to self error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), "Cannot share query to self") + }) + + t.Run("Query set to public and shared to user(s) at same time error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2}, + Public: true, + } + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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{ + { + ID: 1, + 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}/share", 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + ID: 2, + 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{ + { + 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, + }, + }, + }, + }, 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{ + { + ID: 1, + 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}/share", 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, + }, + }, + }, + }, 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{ + ID: 1, + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).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) + 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}/share", 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: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + }, temp.Data) + }) + + t.Run("Shared query set to private success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + ID: 2, + 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}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + + require.Equal(t, http.StatusNoContent, response.Code) + }) + + t.Run("Private query shared to user(s) success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{userId2, userId3}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(true, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: true, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().CreateSavedQueryPermissionsBatch(gomock.Any(), []model.SavedQueriesPermissions{ + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId3), + }, + }).Return([]model.SavedQueriesPermissions{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId2), + }, + { + ID: 2, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(userId3), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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) + }) + + 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{ + ID: 1, + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithOwnerId(userId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + 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}/share", 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}/share", 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}/share", 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}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusNoContent, response.Code) + }) +} + +func TestResources_SharedSavedQueries_Admin(t *testing.T) { + + var ( + mockCtrl = gomock.NewController(t) + mockDB = mocks.NewMockDatabase(mockCtrl) + resources = v2.Resources{DB: mockDB} + ) + + defer mockCtrl.Finish() + + endpoint := "/api/v2/saved-queries/%s/share" + savedQueryId := "1" + adminUserId, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUserId, err := uuid2.NewV4() + require.Nil(t, err) + nonAdminUserId2, err := uuid2.NewV4() + require.Nil(t, err) + + t.Run("Query set to public and shared to user(s) at same time error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUserId}, + Public: true, + } + + // createContextWithAdminOwnerId + // createContextWithOwnerIdAsAdmin + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + + router.ServeHTTP(response, req) + require.Equal(t, http.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), "Public cannot be true while user_ids is populated") + }) + + t.Run("Admin, public query shared to user(s) incorrectly error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusBadRequest, response.Code) + require.Contains(t, response.Body.String(), "Public query cannot be shared to users. You must set your query to private first") + }) + + t.Run("Admin, private query set to public error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + + }) + + t.Run("Admin, private query shared to user(s) error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUserId}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) + + t.Run("Admin, shared query set to public error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) + + t.Run("Admin, shared query shared to user(s) error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{nonAdminUserId, nonAdminUserId2}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) + + t.Run("Admin, shared query set to priavte error", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: false, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: false, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", resources.ShareSavedQueries).Methods("PUT") + + response := httptest.NewRecorder() + router.ServeHTTP(response, req) + require.Equal(t, http.StatusForbidden, response.Code) + }) + + t.Run("Admin, public query set to public success", func(t *testing.T) { + payload := v2.SavedQueryPermissionRequest{ + UserIDs: []uuid2.UUID{}, + Public: true, + } + + mockDB.EXPECT().SavedQueryBelongsToUser(gomock.Any(), gomock.Any(), gomock.Any()).Return(false, nil) + mockDB.EXPECT().GetScopeForSavedQuery(gomock.Any(), gomock.Any(), gomock.Any()).Return(database.SavedQueryScopeMap{ + model.SavedQueryScopeOwned: false, + model.SavedQueryScopePublic: true, + model.SavedQueryScopeShared: false, + }, nil) + mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + ID: 2, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId2), + }, + }, nil) + + req2, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) + require.Nil(t, err) + req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + response2 := httptest.NewRecorder() + // Using the same router as the first request + router.ServeHTTP(response2, req2) + require.Equal(t, http.StatusCreated, response2.Code) + + bodyBytes2, err := io.ReadAll(response2.Body) + require.Nil(t, err) + + var temp2 struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes2, &temp2) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + ID: 1, + SharedToUserID: database.NullUUID(nonAdminUserId), + QueryID: 1, + Public: false, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + { + ID: 2, + SharedToUserID: database.NullUUID(nonAdminUserId2), + QueryID: 1, + Public: false, + 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(nonAdminUserId), + QueryID: 1, + Public: false, + 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{ + ID: 1, + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + ID: 2, + 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + ID: 2, + 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + ID: 2, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId2), + }, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(nonAdminUserId), + QueryID: 1, + Public: false, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + { + ID: 2, + SharedToUserID: database.NullUUID(nonAdminUserId2), + QueryID: 1, + Public: false, + 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{ + ID: 1, + QueryID: 1, + SharedToUserID: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + Public: true, + }, nil) + + req, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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: uuid2.NullUUID{ + UUID: uuid2.UUID{}, + Valid: false, + }, + QueryID: 1, + Public: true, + 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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{ + { + ID: 1, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId), + }, + { + ID: 2, + QueryID: int64(1), + Public: false, + SharedToUserID: database.NullUUID(nonAdminUserId2), + }, + }, nil) + + req2, err := http.NewRequestWithContext(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload2)) + require.Nil(t, err) + req2.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + response2 := httptest.NewRecorder() + // Using the same router as the first request + router.ServeHTTP(response2, req2) + require.Equal(t, http.StatusCreated, response2.Code) + + bodyBytes2, err := io.ReadAll(response2.Body) + require.Nil(t, err) + + var temp2 struct { + Data v2.ShareSavedQueriesResponse `json:"data"` + } + err = json.Unmarshal(bodyBytes2, &temp2) + require.Nil(t, err) + + parsedTime, err := time.Parse(time.RFC3339, "0001-01-01T00:00:00Z") + require.Nil(t, err) + + require.Equal(t, v2.ShareSavedQueriesResponse{ + { + ID: 1, + SharedToUserID: database.NullUUID(nonAdminUserId), + QueryID: 1, + Public: false, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, + }, + }, + { + ID: 2, + SharedToUserID: database.NullUUID(nonAdminUserId2), + QueryID: 1, + Public: false, + 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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(createContextWithAdminOwnerId(adminUserId), "PUT", fmt.Sprintf(endpoint, savedQueryId), must.MarshalJSONReader(payload)) + require.Nil(t, err) + req.Header.Set(headers.ContentType.String(), mediatypes.ApplicationJson.String()) + + router := mux.NewRouter() + router.HandleFunc("/api/v2/saved-queries/{saved_query_id}/share", 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 5cf7ae1544..b06b537165 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -26,6 +26,7 @@ import ( time "time" uuid "github.com/gofrs/uuid" + database "github.com/specterops/bloodhound/src/database" model "github.com/specterops/bloodhound/src/model" appcfg "github.com/specterops/bloodhound/src/model/appcfg" gomock "go.uber.org/mock/gomock" @@ -347,6 +348,21 @@ func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionToUser(arg0, arg1, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionToUser", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionToUser), arg0, arg1, arg2) } +// CreateSavedQueryPermissionsBatch mocks base method. +func (m *MockDatabase) CreateSavedQueryPermissionsBatch(arg0 context.Context, arg1 []model.SavedQueriesPermissions) ([]model.SavedQueriesPermissions, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateSavedQueryPermissionsBatch", arg0, arg1) + ret0, _ := ret[0].([]model.SavedQueriesPermissions) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateSavedQueryPermissionsBatch indicates an expected call of CreateSavedQueryPermissionsBatch. +func (mr *MockDatabaseMockRecorder) CreateSavedQueryPermissionsBatch(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSavedQueryPermissionsBatch", reflect.TypeOf((*MockDatabase)(nil).CreateSavedQueryPermissionsBatch), arg0, arg1) +} + // CreateUser mocks base method. func (m *MockDatabase) CreateUser(arg0 context.Context, arg1 model.User) (model.User, error) { m.ctrl.T.Helper() @@ -545,6 +561,34 @@ func (mr *MockDatabaseMockRecorder) DeleteSavedQuery(arg0, arg1 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQuery", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQuery), arg0, arg1) } +// DeleteSavedQueryPermissionPublic mocks base method. +func (m *MockDatabase) DeleteSavedQueryPermissionPublic(arg0 context.Context, arg1 int64) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionPublic", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// 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, "DeleteSavedQueryPermissionPublic", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionPublic), arg0, arg1) +} + +// DeleteSavedQueryPermissionsForUser mocks base method. +func (m *MockDatabase) DeleteSavedQueryPermissionsForUser(arg0 context.Context, arg1 int64, arg2 uuid.UUID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteSavedQueryPermissionsForUser", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteSavedQueryPermissionsForUser indicates an expected call of DeleteSavedQueryPermissionsForUser. +func (mr *MockDatabaseMockRecorder) DeleteSavedQueryPermissionsForUser(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteSavedQueryPermissionsForUser", reflect.TypeOf((*MockDatabase)(nil).DeleteSavedQueryPermissionsForUser), arg0, arg1, arg2) +} + // DeleteUser mocks base method. func (m *MockDatabase) DeleteUser(arg0 context.Context, arg1 model.User) error { m.ctrl.T.Helper() @@ -1027,10 +1071,10 @@ func (mr *MockDatabaseMockRecorder) GetPermission(arg0, arg1 interface{}) *gomoc } // GetPermissionsForSavedQuery mocks base method. -func (m *MockDatabase) GetPermissionsForSavedQuery(arg0 context.Context, arg1 int64) (model.SavedQueriesPermissions, error) { +func (m *MockDatabase) GetPermissionsForSavedQuery(arg0 context.Context, arg1 int64) ([]model.SavedQueriesPermissions, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "GetPermissionsForSavedQuery", arg0, arg1) - ret0, _ := ret[0].(model.SavedQueriesPermissions) + ret0, _ := ret[0].([]model.SavedQueriesPermissions) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -1131,6 +1175,21 @@ func (mr *MockDatabaseMockRecorder) GetSavedQuery(arg0, arg1 interface{}) *gomoc 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() + ret := m.ctrl.Call(m, "GetScopeForSavedQuery", arg0, arg1, arg2) + ret0, _ := ret[0].(database.SavedQueryScopeMap) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetScopeForSavedQuery indicates an expected call of GetScopeForSavedQuery. +func (mr *MockDatabaseMockRecorder) GetScopeForSavedQuery(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetScopeForSavedQuery", reflect.TypeOf((*MockDatabase)(nil).GetScopeForSavedQuery), arg0, arg1, arg2) +} + // GetSharedSavedQueries mocks base method. func (m *MockDatabase) GetSharedSavedQueries(arg0 context.Context, arg1 uuid.UUID) (model.SavedQueries, error) { m.ctrl.T.Helper() @@ -1279,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 e61bf6c660..840ef569ec 100644 --- a/cmd/api/src/database/saved_queries_permissions.go +++ b/cmd/api/src/database/saved_queries_permissions.go @@ -21,16 +21,26 @@ import ( "github.com/gofrs/uuid" "github.com/specterops/bloodhound/src/model" + "gorm.io/gorm" + "gorm.io/gorm/clause" ) // SavedQueriesPermissionsData methods representing the database interactions pertaining to the saved_queries_permissions model type SavedQueriesPermissionsData interface { CreateSavedQueryPermissionToUser(ctx context.Context, queryID int64, userID uuid.UUID) (model.SavedQueriesPermissions, error) CreateSavedQueryPermissionToPublic(ctx context.Context, queryID int64) (model.SavedQueriesPermissions, error) + CreateSavedQueryPermissionsBatch(ctx context.Context, savedQueryPermissions []model.SavedQueriesPermissions) ([]model.SavedQueriesPermissions, error) CheckUserHasPermissionToSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (bool, error) - GetPermissionsForSavedQuery(ctx context.Context, queryID int64) (model.SavedQueriesPermissions, error) + GetPermissionsForSavedQuery(ctx context.Context, queryID int64) ([]model.SavedQueriesPermissions, error) + GetScopeForSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (SavedQueryScopeMap, error) + DeleteSavedQueryPermissionPublic(ctx context.Context, queryID int64) error + DeleteSavedQueryPermissionsForUser(ctx context.Context, queryID int64, userID uuid.UUID) error + IsSavedQueryShared(ctx context.Context, queryID int64) (bool, error) } +// SavedQueryScopeMap holds the information of a saved query's scope [IE: owned, shared, public] +type SavedQueryScopeMap map[model.SavedQueryScope]bool + // CreateSavedQueryPermissionToUser creates a new entry to the SavedQueriesPermissions table granting a provided user id to access a provided query func (s *BloodhoundDB) CreateSavedQueryPermissionToUser(ctx context.Context, queryID int64, userID uuid.UUID) (model.SavedQueriesPermissions, error) { permission := model.SavedQueriesPermissions{ @@ -49,19 +59,97 @@ 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 +func (s *BloodhoundDB) CreateSavedQueryPermissionsBatch(ctx context.Context, savedQueryPermissions []model.SavedQueriesPermissions) ([]model.SavedQueriesPermissions, error) { + result := s.db.WithContext(ctx).Clauses(clause.OnConflict{ + DoNothing: true, + }).CreateInBatches(&savedQueryPermissions, 100) + + return savedQueryPermissions, CheckError(result) } // CheckUserHasPermissionToSavedQuery returns true or false depending on if the given userID has permission to read the given queryID func (s *BloodhoundDB) CheckUserHasPermissionToSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (bool, error) { - result := s.db.WithContext(ctx).Where("query_id = ? AND user_id = ?", queryID, userID).Or("query_id = ? AND public = true", queryID).Limit(1) + rows := int64(0) + result := s.db.WithContext(ctx).Table("saved_queries_permissions").Select("*").Where("query_id = ? AND shared_to_user_id = ?", queryID, userID).Or("query_id = ? AND public = true", queryID).Limit(1).Count(&rows) - return result.RowsAffected > 0, CheckError(result) + return rows > 0, CheckError(result) } // GetPermissionsForSavedQuery gets all permissions associated with the provided query ID -func (s *BloodhoundDB) GetPermissionsForSavedQuery(ctx context.Context, queryID int64) (model.SavedQueriesPermissions, error) { - queryPermissions := model.SavedQueriesPermissions{QueryID: queryID} +func (s *BloodhoundDB) GetPermissionsForSavedQuery(ctx context.Context, queryID int64) ([]model.SavedQueriesPermissions, error) { + queryPermissions := make([]model.SavedQueriesPermissions, 0) result := s.db.WithContext(ctx).Where("query_id = ?", queryID).Find(&queryPermissions) + return queryPermissions, CheckError(result) } + +// GetScopeForSavedQuery will return a map of the possible scopes given a query id and a user id +func (s *BloodhoundDB) GetScopeForSavedQuery(ctx context.Context, queryID int64, userID uuid.UUID) (SavedQueryScopeMap, error) { + scopes := SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + } + + // Check if the query was shared with the user publicly + publicCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries_permissions").Where("public = true AND query_id = ?", queryID).Count(&publicCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if publicCount > 0 { + scopes[model.SavedQueryScopePublic] = true + } + + // Check if the user owns the query + ownedCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries").Where("id = ? AND user_id = ?", queryID, userID).Count(&ownedCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if ownedCount > 0 { + scopes[model.SavedQueryScopeOwned] = true + } + + // Check if the user has had the query shared to them + sharedCount := int64(0) + if result := s.db.WithContext(ctx).Select("*").Table("saved_queries_permissions").Where("query_id = ? AND shared_to_user_id = ?", queryID, userID).Count(&sharedCount).Limit(1); result.Error != nil { + return scopes, CheckError(result) + } else if sharedCount > 0 { + scopes[model.SavedQueryScopeShared] = true + } + + return scopes, nil +} + +// 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 +func (s *BloodhoundDB) DeleteSavedQueryPermissionsForUser(ctx context.Context, queryID int64, userID uuid.UUID) error { + return CheckError(s.db.WithContext(ctx).Table("saved_queries_permissions").Where("query_id = ? AND shared_to_user_id = ?", queryID, userID).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 92a936cee4..ff39d21da0 100644 --- a/cmd/api/src/database/saved_queries_permissions_test.go +++ b/cmd/api/src/database/saved_queries_permissions_test.go @@ -23,6 +23,9 @@ import ( "context" "testing" + uuid2 "github.com/gofrs/uuid" + "github.com/google/uuid" + "github.com/specterops/bloodhound/src/database" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/test/integration" @@ -30,7 +33,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSavedQueriesPermissions_SharingToUser(t *testing.T) { +func TestSavedQueriesPermissions_CheckUserHasPermissionToSavedQuery(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) @@ -46,34 +49,277 @@ func TestSavedQueriesPermissions_SharingToUser(t *testing.T) { }) require.NoError(t, err) - query, err := dbInst.CreateSavedQuery(testCtx, user.ID, "Test Query", "MATCH(n) RETURN n", "An example Query") + query, err := dbInst.CreateSavedQuery(testCtx, user.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) require.NoError(t, err) - permissions, err := dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) + result, err := dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) require.NoError(t, err) + assert.True(t, result) - assert.Equal(t, database.NullUUID(user2.ID), permissions.SharedToUserID) - assert.Equal(t, false, permissions.Public) - assert.Equal(t, query.ID, permissions.QueryID) + result, err = dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user.ID) + require.NoError(t, err) + assert.False(t, result) } -func TestSavedQueriesPermissions_SharingToGlobal(t *testing.T) { +func TestSavedQueriesPermissions_CreateSavedQueryPermissionsBatch(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) ) + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) - user, err := dbInst.CreateUser(testCtx, model.User{ + users := make([]model.User, 0) + for i := 0; i < 5; i++ { + user, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: uuid.NewString(), + }) + require.NoError(t, err) + users = append(users, user) + } + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + permissions := make([]model.SavedQueriesPermissions, 0) + for _, user := range users { + permissions = append(permissions, model.SavedQueriesPermissions{ + QueryID: query.ID, + Public: false, + SharedToUserID: database.NullUUID(user.ID), + }) + } + + _, err = dbInst.CreateSavedQueryPermissionsBatch(testCtx, permissions) + require.NoError(t, err) + + permissions, err = dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) + require.NoError(t, err) + assert.Len(t, permissions, 5) +} + +func TestSavedQueriesPermissions_CreateSavedQueryPermissionsBatchBadDataError(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + user1, err := dbInst.CreateUser(testCtx, model.User{ PrincipalName: userPrincipal, }) require.NoError(t, err) - query, err := dbInst.CreateSavedQuery(testCtx, user.ID, "Test Query", "MATCH(n) RETURN n", "An example Query") + users := make([]model.User, 0) + for i := 0; i < 5; i++ { + user, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: uuid.NewString(), + }) + require.NoError(t, err) + users = append(users, user) + } + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") require.NoError(t, err) - permissions, err := dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) + permissions := make([]model.SavedQueriesPermissions, 0) + for _, user := range users { + permissions = append(permissions, model.SavedQueriesPermissions{ + QueryID: query.ID, + Public: false, + SharedToUserID: database.NullUUID(user.ID), + }) + } + + invalidUUID, _ := uuid2.NewV4() + permissions[3].SharedToUserID = database.NullUUID(invalidUUID) + + _, err = dbInst.CreateSavedQueryPermissionsBatch(testCtx, permissions) + require.Error(t, err) + + permissions, err = dbInst.GetPermissionsForSavedQuery(testCtx, query.ID) require.NoError(t, err) - assert.Equal(t, true, permissions.Public) - assert.Equal(t, query.ID, permissions.QueryID) + assert.Len(t, permissions, 0) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryPublic(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user2.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToPublic(testCtx, query.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: true, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: false, + }, scope) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryShared(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user2.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: false, + model.SavedQueryScopeShared: true, + }, scope) +} + +func TestSavedQueriesPermissions_GetScopeForSavedQueryOwned(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.CreateSavedQueryPermissionToUser(testCtx, query.ID, user2.ID) + require.NoError(t, err) + + scope, err := dbInst.GetScopeForSavedQuery(testCtx, query.ID, user1.ID) + require.NoError(t, err) + + require.Equal(t, database.SavedQueryScopeMap{ + model.SavedQueryScopePublic: false, + model.SavedQueryScopeOwned: true, + model.SavedQueryScopeShared: false, + }, scope) +} + +func TestSavedQueriesPermissions_DeleteSavedQueryPermissionsForUser(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.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) + require.NoError(t, err) + + hasPermission, err = dbInst.CheckUserHasPermissionToSavedQuery(testCtx, query.ID, user2.ID) + require.NoError(t, err) + assert.False(t, hasPermission) +} + +func TestSavedQueriesPermissions_DeleteSavedQueryPermissionPublic(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + 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) + require.NoError(t, err) + + err = dbInst.DeleteSavedQueryPermissionPublic(testCtx, query.ID) + require.NoError(t, err) +} + +func TestSavedQueriesPermissions_IsSavedQueryShared(t *testing.T) { + var ( + testCtx = context.Background() + dbInst = integration.SetupDB(t) + ) + + user1, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + user2, err := dbInst.CreateUser(testCtx, model.User{ + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + query, err := dbInst.CreateSavedQuery(testCtx, user1.ID, "Test Query", "TESTING", "Example") + require.NoError(t, err) + + _, err = dbInst.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) } diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index e30335bdf5..43687e3561 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4437,6 +4437,93 @@ } } }, + "/api/v2/saved-queries/{saved_query_id}/share": { + "parameters": [ + { + "$ref": "#/components/parameters/header.prefer" + }, + { + "name": "saved_query_id", + "description": "ID of the saved query", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "put": { + "operationId": "ShareSavedQuery", + "summary": "Share a saved query or set it to public", + "description": "Shares an existing saved query or makes it public", + "tags": [ + "Cypher", + "Community", + "Enterprise" + ], + "requestBody": { + "description": "The request body for sharing a saved query", + "required": true, + "content": { + "application/json": { + "schema": { + "properties": { + "user_ids": { + "type": "array", + "items": { + "type": "string", + "format": "uuid" + } + }, + "public": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "201": { + "description": "Created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/model.saved-queries-permissions" + } + } + } + } + } + } + }, + "400": { + "$ref": "#/components/responses/bad-request" + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not-found" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + } + }, "/api/v2/graphs/cypher": { "parameters": [ { @@ -14670,6 +14757,42 @@ } ] }, + "model.saved-queries-permissions": { + "allOf": [ + { + "$ref": "#/components/schemas/model.components.int64.id" + }, + { + "$ref": "#/components/schemas/model.components.timestamps" + }, + { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "shared_to_user_id": { + "allOf": [ + { + "$ref": "#/components/schemas/null.uuid" + }, + { + "readOnly": true + } + ] + }, + "query_id": { + "type": "integer", + "format": "int64" + }, + "public": { + "type": "boolean" + } + } + } + ] + }, "api.response.time-window": { "type": "object", "properties": { diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index 6308f29a8f..c23bb978f9 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -324,6 +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/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.share.yaml new file mode 100644 index 0000000000..dcab69fcc5 --- /dev/null +++ b/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml @@ -0,0 +1,72 @@ +# 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 + +parameters: + - $ref: './../parameters/header.prefer.yaml' + - name: saved_query_id + description: ID of the saved query + in: path + required: true + schema: + type: integer + format: int64 +put: + operationId: ShareSavedQuery + summary: Share a saved query or set it to public + description: Shares an existing saved query or makes it public + tags: + - Cypher + - Community + - Enterprise + requestBody: + description: The request body for sharing a saved query + required: true + content: + application/json: + schema: + properties: + user_ids: + type: array + items: + type: string + format: uuid + public: + type: boolean + + responses: + 201: + description: Created + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + $ref: './../schemas/model.saved-queries-permissions.yaml' + 400: + $ref: './../responses/bad-request.yaml' + 401: + $ref: './../responses/unauthorized.yaml' + 403: + $ref: './../responses/forbidden.yaml' + 404: + $ref: './../responses/not-found.yaml' + 429: + $ref: './../responses/too-many-requests.yaml' + 500: + $ref: './../responses/internal-server-error.yaml' \ No newline at end of file diff --git a/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml new file mode 100644 index 0000000000..35812c0d46 --- /dev/null +++ b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml @@ -0,0 +1,33 @@ +# 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: + id: + type: integer + format: int64 + shared_to_user_id: + allOf: + - $ref: './null.uuid.yaml' + - readOnly: true + query_id: + type: integer + format: int64 + public: + type: boolean \ No newline at end of file From e13036a87235ec83709ff1da63c9c54ae8e3fbf3 Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Tue, 20 Aug 2024 10:17:00 -0700 Subject: [PATCH 2/5] Addressed previous PR feedback and adjusted unit tests --- cmd/api/src/api/v2/saved_queries_test.go | 282 +++++++++--------- .../src/model/saved_queries_permissions.go | 3 +- 2 files changed, 145 insertions(+), 140 deletions(-) diff --git a/cmd/api/src/api/v2/saved_queries_test.go b/cmd/api/src/api/v2/saved_queries_test.go index 760ed502cd..6425702ce0 100644 --- a/cmd/api/src/api/v2/saved_queries_test.go +++ b/cmd/api/src/api/v2/saved_queries_test.go @@ -1690,7 +1690,6 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(userId2), @@ -1734,13 +1733,11 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, }).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), @@ -1770,30 +1767,34 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { 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, + BigSerial: model.BigSerial{ + ID: 0, + 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, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -1821,7 +1822,6 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(userId2), @@ -1854,16 +1854,18 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { 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, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -1884,7 +1886,6 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, nil) mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - ID: 1, QueryID: 1, SharedToUserID: uuid2.NullUUID{ UUID: uuid2.UUID{}, @@ -1894,13 +1895,11 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, nil) mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).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), @@ -1934,19 +1933,21 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 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, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -1968,13 +1969,11 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).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), @@ -2024,13 +2023,11 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, }).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), @@ -2063,30 +2060,34 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { 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, + BigSerial: model.BigSerial{ + ID: 0, + 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, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -2107,7 +2108,6 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { }, nil) mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - ID: 1, QueryID: 1, SharedToUserID: uuid2.NullUUID{ UUID: uuid2.UUID{}, @@ -2142,19 +2142,21 @@ func TestResources_SharedSavedQueries_NonAdmin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 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, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -2600,7 +2602,6 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), @@ -2644,13 +2645,11 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), }, { - ID: 2, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId2), @@ -2680,30 +2679,34 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 1, SharedToUserID: database.NullUUID(nonAdminUserId), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, { - ID: 2, SharedToUserID: database.NullUUID(nonAdminUserId2), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -2731,7 +2734,6 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), @@ -2764,16 +2766,18 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 1, SharedToUserID: database.NullUUID(nonAdminUserId), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -2794,7 +2798,6 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, nil) mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - ID: 1, QueryID: 1, SharedToUserID: uuid2.NullUUID{ UUID: uuid2.UUID{}, @@ -2804,13 +2807,11 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, nil) mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), }, { - ID: 2, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId2), @@ -2844,19 +2845,21 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 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, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -2878,13 +2881,11 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(true, nil) mockDB.EXPECT().GetPermissionsForSavedQuery(gomock.Any(), gomock.Any()).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), }, { - ID: 2, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId2), @@ -2934,13 +2935,11 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), }, { - ID: 2, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId2), @@ -2973,30 +2972,34 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 1, SharedToUserID: database.NullUUID(nonAdminUserId), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, { - ID: 2, SharedToUserID: database.NullUUID(nonAdminUserId2), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -3017,7 +3020,6 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, nil) mockDB.EXPECT().IsSavedQueryShared(gomock.Any(), gomock.Any()).Return(false, nil) mockDB.EXPECT().CreateSavedQueryPermissionToPublic(gomock.Any(), int64(1)).Return(model.SavedQueriesPermissions{ - ID: 1, QueryID: 1, SharedToUserID: uuid2.NullUUID{ UUID: uuid2.UUID{}, @@ -3052,19 +3054,21 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 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, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, @@ -3177,13 +3181,11 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { }, }).Return([]model.SavedQueriesPermissions{ { - ID: 1, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId), }, { - ID: 2, QueryID: int64(1), Public: false, SharedToUserID: database.NullUUID(nonAdminUserId2), @@ -3213,30 +3215,34 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { require.Equal(t, v2.ShareSavedQueriesResponse{ { - ID: 1, SharedToUserID: database.NullUUID(nonAdminUserId), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, { - ID: 2, SharedToUserID: database.NullUUID(nonAdminUserId2), QueryID: 1, Public: false, - Basic: model.Basic{ - CreatedAt: parsedTime, - UpdatedAt: parsedTime, - DeletedAt: sql.NullTime{ - Time: parsedTime, - Valid: false, + BigSerial: model.BigSerial{ + ID: 0, + Basic: model.Basic{ + CreatedAt: parsedTime, + UpdatedAt: parsedTime, + DeletedAt: sql.NullTime{ + Time: parsedTime, + Valid: false, + }, }, }, }, diff --git a/cmd/api/src/model/saved_queries_permissions.go b/cmd/api/src/model/saved_queries_permissions.go index 7513f8a5a7..3252a15f17 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 } From 0fc97a69d2216a65c88ade0e3019a6ecf9cce529 Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Tue, 20 Aug 2024 10:39:35 -0700 Subject: [PATCH 3/5] Corrected openapi stuff --- packages/go/openapi/doc/openapi.json | 9 ++++----- .../openapi/src/paths/cypher.saved-queries.id.share.yaml | 4 +++- .../src/schemas/model.saved-queries-permissions.yaml | 3 --- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 43687e3561..b952daae10 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4463,7 +4463,7 @@ "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": { @@ -4503,6 +4503,9 @@ } } }, + "204": { + "$ref": "#/components/responses/no-content" + }, "400": { "$ref": "#/components/responses/bad-request" }, @@ -14768,10 +14771,6 @@ { "type": "object", "properties": { - "id": { - "type": "integer", - "format": "int64" - }, "shared_to_user_id": { "allOf": [ { diff --git a/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml b/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml index dcab69fcc5..aeaa2572b6 100644 --- a/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml +++ b/packages/go/openapi/src/paths/cypher.saved-queries.id.share.yaml @@ -32,7 +32,7 @@ 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: @@ -58,6 +58,8 @@ put: type: array items: $ref: './../schemas/model.saved-queries-permissions.yaml' + 204: + $ref: './../responses/no-content.yaml' 400: $ref: './../responses/bad-request.yaml' 401: diff --git a/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml index 35812c0d46..d9590ac2da 100644 --- a/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml +++ b/packages/go/openapi/src/schemas/model.saved-queries-permissions.yaml @@ -19,9 +19,6 @@ allOf: - $ref: './model.components.timestamps.yaml' - type: object properties: - id: - type: integer - format: int64 shared_to_user_id: allOf: - $ref: './null.uuid.yaml' From 2205fd56f85f820230ea82c8e9c3c06712411f3c Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Tue, 20 Aug 2024 16:26:19 -0700 Subject: [PATCH 4/5] Changed the endpoint url --- cmd/api/src/api/registration/v2.go | 2 +- cmd/api/src/api/v2/saved_queries.go | 232 ---------------- .../src/api/v2/saved_queries_permissions.go | 262 ++++++++++++++++++ cmd/api/src/api/v2/saved_queries_test.go | 78 +++--- packages/go/openapi/doc/openapi.json | 2 +- packages/go/openapi/src/openapi.yaml | 2 +- 6 files changed, 304 insertions(+), 274 deletions(-) create mode 100644 cmd/api/src/api/v2/saved_queries_permissions.go diff --git a/cmd/api/src/api/registration/v2.go b/cmd/api/src/api/registration/v2.go index fc0124f620..cf6f142c15 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 7a7d7418eb..5a42c820c5 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,234 +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.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_permissions.go b/cmd/api/src/api/v2/saved_queries_permissions.go new file mode 100644 index 0000000000..94e3c176df --- /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 6425702ce0..edc285badb 100644 --- a/cmd/api/src/api/v2/saved_queries_test.go +++ b/cmd/api/src/api/v2/saved_queries_test.go @@ -1526,7 +1526,7 @@ func TestResources_ShareSavedQueries_SavingPermissionsErrors(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) @@ -1566,7 +1566,7 @@ func TestResources_ShareSavedQueries_SavingPermissionsErrors(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) @@ -1583,7 +1583,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1612,7 +1612,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1639,7 +1639,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1659,7 +1659,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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() @@ -1701,7 +1701,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1834,7 +1834,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1913,7 +1913,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -1989,7 +1989,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2040,7 +2040,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2122,7 +2122,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2183,7 +2183,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2210,7 +2210,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2236,7 +2236,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2262,7 +2262,7 @@ func TestResources_SharedSavedQueries_NonAdmin(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) @@ -2280,7 +2280,7 @@ func TestResources_SharedSavedQueries_Admin(t *testing.T) { defer mockCtrl.Finish() - endpoint := "/api/v2/saved-queries/%s/share" + endpoint := "/api/v2/saved-queries/%s/permissions" savedQueryId := "1" adminUserId, err := uuid2.NewV4() require.Nil(t, err) @@ -2303,7 +2303,7 @@ func TestResources_SharedSavedQueries_Admin(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() @@ -2331,7 +2331,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2358,7 +2358,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2385,7 +2385,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2412,7 +2412,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2438,7 +2438,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2464,7 +2464,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2490,7 +2490,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2516,7 +2516,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2543,7 +2543,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2571,7 +2571,7 @@ func TestResources_SharedSavedQueries_Admin(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() @@ -2613,7 +2613,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2746,7 +2746,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2825,7 +2825,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2901,7 +2901,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -2952,7 +2952,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3034,7 +3034,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3095,7 +3095,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3121,7 +3121,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3149,7 +3149,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3269,7 +3269,7 @@ func TestResources_SharedSavedQueries_Admin(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) @@ -3295,7 +3295,7 @@ func TestResources_SharedSavedQueries_Admin(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) diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index b952daae10..763e8faacc 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4437,7 +4437,7 @@ } } }, - "/api/v2/saved-queries/{saved_query_id}/share": { + "/api/v2/saved-queries/{saved_query_id}/permissions": { "parameters": [ { "$ref": "#/components/parameters/header.prefer" diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index c23bb978f9..5a3f4b48dc 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -324,7 +324,7 @@ 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: + /api/v2/saved-queries/{saved_query_id}/permissions: $ref: './paths/cypher.saved-queries.id.share.yaml' /api/v2/graphs/cypher: $ref: './paths/cypher.graphs.cypher.yaml' From 378327009018c9e4626ff77a7b1b999e5e22185d Mon Sep 17 00:00:00 2001 From: ALCooper12 Date: Tue, 20 Aug 2024 18:38:32 -0700 Subject: [PATCH 5/5] More openapi corrections and file name change --- packages/go/openapi/doc/openapi.json | 3 ++- packages/go/openapi/src/openapi.yaml | 2 +- ....id.share.yaml => cypher.saved-queries.id.permissions.yaml} | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) rename packages/go/openapi/src/paths/{cypher.saved-queries.id.share.yaml => cypher.saved-queries.id.permissions.yaml} (97%) diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 763e8faacc..08111f8523 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -4449,7 +4449,7 @@ "required": true, "schema": { "type": "integer", - "format": "int64" + "format": "int32" } } ], @@ -4468,6 +4468,7 @@ "content": { "application/json": { "schema": { + "type": "object", "properties": { "user_ids": { "type": "array", diff --git a/packages/go/openapi/src/openapi.yaml b/packages/go/openapi/src/openapi.yaml index 5a3f4b48dc..3b2167ff76 100644 --- a/packages/go/openapi/src/openapi.yaml +++ b/packages/go/openapi/src/openapi.yaml @@ -325,7 +325,7 @@ paths: /api/v2/saved-queries/{saved_query_id}: $ref: './paths/cypher.saved-queries.id.yaml' /api/v2/saved-queries/{saved_query_id}/permissions: - $ref: './paths/cypher.saved-queries.id.share.yaml' + $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 97% 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 aeaa2572b6..eb1b9568e3 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 @@ -37,6 +37,7 @@ put: content: application/json: schema: + type: object properties: user_ids: type: array