diff --git a/cmd/api/src/api/v2/ad_entity.go b/cmd/api/src/api/v2/ad_entity.go index 79882117ca8..cce55b3a0d5 100644 --- a/cmd/api/src/api/v2/ad_entity.go +++ b/cmd/api/src/api/v2/ad_entity.go @@ -18,11 +18,15 @@ package v2 import ( "fmt" + "log/slog" "net/http" "github.com/specterops/bloodhound/cmd/api/src/api" + "github.com/specterops/bloodhound/cmd/api/src/auth" + bhCtx "github.com/specterops/bloodhound/cmd/api/src/ctx" adAnalysis "github.com/specterops/bloodhound/packages/go/analysis/ad" "github.com/specterops/bloodhound/packages/go/analysis/tiering" + "github.com/specterops/bloodhound/packages/go/bhlog/attr" "github.com/specterops/bloodhound/packages/go/graphschema/ad" "github.com/specterops/bloodhound/packages/go/graphschema/common" "github.com/specterops/dawgs/graph" @@ -64,10 +68,22 @@ func (s *Resources) PatchDomain(response http.ResponseWriter, request *http.Requ } func (s *Resources) handleAdEntityInfoQuery(response http.ResponseWriter, request *http.Request, entityType graph.Kind, countQueries map[string]any) { + user, isUser := auth.GetUserFromAuthCtx(bhCtx.FromRequest(request).AuthCtx) + if !isUser { + slog.Error("Unable to get user from auth context") + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + return + } + if includeCounts, err := api.ParseOptionalBool(request.URL.Query().Get(api.QueryParameterIncludeCounts), true); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsBadQueryParameterFilters, request), response) } else if objectId, err := GetEntityObjectIDFromRequestPath(request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("error reading objectid: %v", err), request), response) + } else if hasAccess, err := CheckUserHasAccessToNodeById(request.Context(), s.DB, s.GraphQuery, s.DogTags, user, objectId, entityType); err != nil { + slog.ErrorContext(request.Context(), "Error checking if user has access to node for ETAC", attr.Error(err)) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + } else if !hasAccess { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) } else if node, err := s.GraphQuery.GetEntityByObjectId(request.Context(), objectId, entityType); err != nil { if graph.IsErrNotFound(err) { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, "node not found", request), response) diff --git a/cmd/api/src/api/v2/ad_entity_test.go b/cmd/api/src/api/v2/ad_entity_test.go index 8be33efde3f..dd03349d15b 100644 --- a/cmd/api/src/api/v2/ad_entity_test.go +++ b/cmd/api/src/api/v2/ad_entity_test.go @@ -28,8 +28,14 @@ import ( "github.com/specterops/bloodhound/cmd/api/src/api" v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2" "github.com/specterops/bloodhound/cmd/api/src/api/v2/apitest" + "github.com/specterops/bloodhound/cmd/api/src/auth" + "github.com/specterops/bloodhound/cmd/api/src/ctx" + mocks_db "github.com/specterops/bloodhound/cmd/api/src/database/mocks" + "github.com/specterops/bloodhound/cmd/api/src/model" "github.com/specterops/bloodhound/cmd/api/src/queries/mocks" + "github.com/specterops/bloodhound/cmd/api/src/services/dogtags" "github.com/specterops/bloodhound/cmd/api/src/utils/test" + "github.com/specterops/bloodhound/packages/go/graphschema/ad" "github.com/specterops/bloodhound/packages/go/headers" "github.com/specterops/bloodhound/packages/go/mediatypes" "github.com/specterops/dawgs/graph" @@ -41,7 +47,15 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -49,6 +63,9 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -59,6 +76,7 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -69,6 +87,7 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -84,6 +103,7 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -99,6 +119,7 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -117,6 +138,7 @@ func TestResources_GetComputerEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -134,7 +156,14 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -142,6 +171,9 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -152,6 +184,7 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -162,6 +195,7 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -177,6 +211,7 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -192,6 +227,7 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -210,6 +246,7 @@ func TestResources_GetDomainEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -333,7 +370,14 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -341,6 +385,9 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -351,6 +398,7 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -361,6 +409,7 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -376,6 +425,7 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -391,6 +441,7 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -409,6 +460,7 @@ func TestResources_GetGPOEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -426,7 +478,14 @@ func TestResources_GetOUEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -434,6 +493,9 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -444,6 +506,7 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -454,6 +517,7 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -469,6 +533,7 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -484,6 +549,7 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -502,6 +568,7 @@ func TestResources_GetOUEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -519,7 +586,14 @@ func TestResources_GetUserEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -527,6 +601,9 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -537,6 +614,7 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -547,6 +625,7 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -562,6 +641,7 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -577,6 +657,7 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -595,6 +676,7 @@ func TestResources_GetUserEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -612,7 +694,14 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { var ( mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph} + resources = v2.Resources{GraphQuery: mockGraph, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} + bheCtx = ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } ) defer mockCtrl.Finish() @@ -620,6 +709,9 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Run([]apitest.Case{ { Name: "NoObjectID", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "error reading objectid:") @@ -630,6 +722,7 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "foo") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) @@ -640,6 +733,7 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Name: "GraphDBNotFoundError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -655,6 +749,7 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Name: "GraphDBGetEntityByObjectIdError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -670,6 +765,7 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -688,6 +784,7 @@ func TestResources_GetGroupEntityInfo(t *testing.T) { Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "counts", "false") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -706,6 +803,7 @@ func TestResources_GetBaseEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -713,10 +811,12 @@ func TestResources_GetBaseEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + dogTagsOverrides dogtags.TestOverrides + user model.User + expected expected } tt := []testData{ @@ -825,6 +925,134 @@ func TestResources_GetBaseEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("Base")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/base/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Entity).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.Entity), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/base/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Entity).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/base/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Entity).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -833,6 +1061,7 @@ func TestResources_GetBaseEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() @@ -840,13 +1069,24 @@ func TestResources_GetBaseEntityInfo(t *testing.T) { resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/base/{%s}", api.URIPathVariableObjectID), resources.GetBaseEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/base/{%s}", api.URIPathVariableObjectID), resources.GetBaseEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -862,6 +1102,7 @@ func TestResources_GetContainerEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -869,10 +1110,12 @@ func TestResources_GetContainerEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + dogTagsOverrides dogtags.TestOverrides + user model.User + expected expected } tt := []testData{ @@ -980,6 +1223,134 @@ func TestResources_GetContainerEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("Container")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/containers/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Container).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Container}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.Container), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/containers/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Container).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.Container}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.Container.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/containers/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Container).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -988,20 +1359,32 @@ func TestResources_GetContainerEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() testCase.setupMocks(t, mocks) + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/containers/{%s}", api.URIPathVariableObjectID), resources.GetContainerEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/containers/{%s}", api.URIPathVariableObjectID), resources.GetContainerEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -1017,6 +1400,7 @@ func TestResources_GetAIACAEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1024,10 +1408,12 @@ func TestResources_GetAIACAEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1135,6 +1521,134 @@ func TestResources_GetAIACAEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("AIACA")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/aiacas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.AIACA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.AIACA}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.AIACA), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/aiacas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.AIACA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.AIACA}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.AIACA.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/aiacas/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.AIACA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -1143,20 +1657,32 @@ func TestResources_GetAIACAEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/aiacas/{%s}", api.URIPathVariableObjectID), resources.GetAIACAEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/aiacas/{%s}", api.URIPathVariableObjectID), resources.GetAIACAEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -1172,6 +1698,7 @@ func TestResources_GetRootCAEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1179,10 +1706,12 @@ func TestResources_GetRootCAEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1282,44 +1811,185 @@ func TestResources_GetRootCAEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("RootCA")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, - } - for _, testCase := range tt { - t.Run(testCase.name, func(t *testing.T) { - t.Parallel() - ctrl := gomock.NewController(t) - - mocks := &mock{ - mockGraphQuery: mocks.NewMockGraph(ctrl), - } - - request := testCase.buildRequest() - - testCase.setupMocks(t, mocks) - - resources := v2.Resources{ - GraphQuery: mocks.mockGraphQuery, - } - - response := httptest.NewRecorder() - - router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/rootcas/{%s}", api.URIPathVariableObjectID), resources.GetRootCAEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) - - status, header, body := test.ProcessResponse(t, response) - - assert.Equal(t, testCase.expected.responseCode, status) - assert.Equal(t, testCase.expected.responseHeader, header) - assert.JSONEq(t, testCase.expected.responseBody, body) - }) - } -} - -func TestResources_GetEnterpriseCAEntityInfo(t *testing.T) { + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/rootcas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.RootCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.RootCA}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.RootCA), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/rootcas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.RootCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.RootCA}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.RootCA.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/rootcas/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.RootCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, + } + for _, testCase := range tt { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + ctrl := gomock.NewController(t) + + mocks := &mock{ + mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), + } + + request := testCase.buildRequest() + + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + + testCase.setupMocks(t, mocks) + + resources := v2.Resources{ + GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), + } + + response := httptest.NewRecorder() + + router := mux.NewRouter() + router.HandleFunc(fmt.Sprintf("/api/v2/rootcas/{%s}", api.URIPathVariableObjectID), resources.GetRootCAEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) + + status, header, body := test.ProcessResponse(t, response) + + assert.Equal(t, testCase.expected.responseCode, status) + assert.Equal(t, testCase.expected.responseHeader, header) + assert.JSONEq(t, testCase.expected.responseBody, body) + }) + } +} + +func TestResources_GetEnterpriseCAEntityInfo(t *testing.T) { t.Parallel() type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1327,10 +1997,12 @@ func TestResources_GetEnterpriseCAEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1438,6 +2110,134 @@ func TestResources_GetEnterpriseCAEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("EnterpriseCA")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/enterprisecas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.EnterpriseCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.EnterpriseCA}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.EnterpriseCA), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/enterprisecas/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.EnterpriseCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.EnterpriseCA}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.EnterpriseCA.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/enterprisecas/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.EnterpriseCA).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -1446,20 +2246,32 @@ func TestResources_GetEnterpriseCAEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/enterprisecas/{%s}", api.URIPathVariableObjectID), resources.GetEnterpriseCAEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/enterprisecas/{%s}", api.URIPathVariableObjectID), resources.GetEnterpriseCAEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -1475,6 +2287,7 @@ func TestResources_GetNTAuthStoreEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1482,10 +2295,12 @@ func TestResources_GetNTAuthStoreEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1593,6 +2408,134 @@ func TestResources_GetNTAuthStoreEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("NTAuthStore")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/ntauthstores/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.NTAuthStore).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.NTAuthStore}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.NTAuthStore), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/ntauthstores/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.NTAuthStore).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.NTAuthStore}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.NTAuthStore.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/ntauthstores/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.NTAuthStore).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -1601,20 +2544,32 @@ func TestResources_GetNTAuthStoreEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/ntauthstores/{%s}", api.URIPathVariableObjectID), resources.GetNTAuthStoreEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/ntauthstores/{%s}", api.URIPathVariableObjectID), resources.GetNTAuthStoreEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -1630,6 +2585,7 @@ func TestResources_GetCertTemplateEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1637,10 +2593,12 @@ func TestResources_GetCertTemplateEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1748,6 +2706,134 @@ func TestResources_GetCertTemplateEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("CertTemplate")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/certtemplates/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.CertTemplate).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.CertTemplate}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.CertTemplate), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/certtemplates/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.CertTemplate).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.CertTemplate}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.CertTemplate.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/certtemplates/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.CertTemplate).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -1756,20 +2842,32 @@ func TestResources_GetCertTemplateEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/certtemplates/{%s}", api.URIPathVariableObjectID), resources.GetCertTemplateEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/certtemplates/{%s}", api.URIPathVariableObjectID), resources.GetCertTemplateEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -1785,6 +2883,7 @@ func TestResources_GetIssuancePolicyEntityInfo(t *testing.T) { type mock struct { mockGraphQuery *mocks.MockGraph + mockDatabase *mocks_db.MockDatabase } type expected struct { responseBody string @@ -1792,10 +2891,12 @@ func TestResources_GetIssuancePolicyEntityInfo(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -1903,6 +3004,134 @@ func TestResources_GetIssuancePolicyEntityInfo(t *testing.T) { mocks.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", graph.StringKind("IssuancePolicy")).Return(graph.NewNode(graph.ID(1), graph.NewProperties()), nil) }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.IssuancePolicy).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.IssuancePolicy}, + Properties: props, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, ad.IssuancePolicy), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id", + RawQuery: "counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + props := graph.AsProperties(map[string]any{ + "domainsid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.IssuancePolicy).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity, ad.IssuancePolicy}, + Properties: props, + }, nil).Times(2) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockGraphQuery.EXPECT().GetEntityCountResults(gomock.Any(), graph.NewNode(graph.ID(16), props, graph.StringsToKinds([]string{ad.Entity.String(), ad.IssuancePolicy.String()})...), gomock.Any()).Return(map[string]any{"results": "output"}) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"results":"output"}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.IssuancePolicy).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -1911,20 +3140,32 @@ func TestResources_GetIssuancePolicyEntityInfo(t *testing.T) { mocks := &mock{ mockGraphQuery: mocks.NewMockGraph(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/issuancepolicies/{%s}", api.URIPathVariableObjectID), resources.GetIssuancePolicyEntityInfo).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/issuancepolicies/{%s}", api.URIPathVariableObjectID), resources.GetIssuancePolicyEntityInfo).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) diff --git a/cmd/api/src/api/v2/ad_related_entity.go b/cmd/api/src/api/v2/ad_related_entity.go index 55cc4533d52..51a7e8cbe41 100644 --- a/cmd/api/src/api/v2/ad_related_entity.go +++ b/cmd/api/src/api/v2/ad_related_entity.go @@ -19,13 +19,17 @@ package v2 import ( "errors" "fmt" + "log/slog" "net/http" "github.com/specterops/bloodhound/cmd/api/src/api" + "github.com/specterops/bloodhound/cmd/api/src/auth" + bhCtx "github.com/specterops/bloodhound/cmd/api/src/ctx" "github.com/specterops/bloodhound/cmd/api/src/model" "github.com/specterops/bloodhound/cmd/api/src/model/appcfg" "github.com/specterops/bloodhound/cmd/api/src/queries" adAnalysis "github.com/specterops/bloodhound/packages/go/analysis/ad" + "github.com/specterops/bloodhound/packages/go/bhlog/attr" "github.com/specterops/bloodhound/packages/go/graphschema/ad" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/ops" @@ -37,8 +41,20 @@ import ( // Path delegates are for graphing, list delegates are for listing and counting. Endpoints // without a certain delegate do not support that delegate feature. func (s *Resources) handleAdRelatedEntityQuery(response http.ResponseWriter, request *http.Request, queryName string, pathDelegate any, listDelegate any) { + user, isUser := auth.GetUserFromAuthCtx(bhCtx.FromRequest(request).AuthCtx) + if !isUser { + slog.Error("Unable to get user from auth context") + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + return + } + if params, err := queries.BuildEntityQueryParams(request, queryName, pathDelegate, listDelegate); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(api.FmtErrorResponseDetailsBadQueryParameters, err), request), response) + } else if hasAccess, err := CheckUserHasAccessToNodeById(request.Context(), s.DB, s.GraphQuery, s.DogTags, user, params.ObjectID, ad.Entity); err != nil { + slog.ErrorContext(request.Context(), "Error checking if user has access to node for ETAC", attr.Error(err)) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + } else if !hasAccess { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) } else if entityPanelCachingFlag, err := s.DB.GetFlagByKey(request.Context(), appcfg.FeatureEntityPanelCaching); err != nil { api.HandleDatabaseError(request, response, err) } else if results, count, err := s.GraphQuery.GetADEntityQueryResult(request.Context(), params, entityPanelCachingFlag.Enabled); err != nil { diff --git a/cmd/api/src/api/v2/ad_related_entity_test.go b/cmd/api/src/api/v2/ad_related_entity_test.go index 5385efc0193..8222ffd1481 100644 --- a/cmd/api/src/api/v2/ad_related_entity_test.go +++ b/cmd/api/src/api/v2/ad_related_entity_test.go @@ -28,11 +28,17 @@ import ( "github.com/specterops/bloodhound/cmd/api/src/api" v2 "github.com/specterops/bloodhound/cmd/api/src/api/v2" "github.com/specterops/bloodhound/cmd/api/src/api/v2/apitest" + "github.com/specterops/bloodhound/cmd/api/src/auth" + "github.com/specterops/bloodhound/cmd/api/src/ctx" dbMocks "github.com/specterops/bloodhound/cmd/api/src/database/mocks" + "github.com/specterops/bloodhound/cmd/api/src/model" "github.com/specterops/bloodhound/cmd/api/src/model/appcfg" "github.com/specterops/bloodhound/cmd/api/src/queries" "github.com/specterops/bloodhound/cmd/api/src/queries/mocks" + "github.com/specterops/bloodhound/cmd/api/src/services/dogtags" "github.com/specterops/bloodhound/cmd/api/src/utils/test" + "github.com/specterops/bloodhound/packages/go/graphschema/ad" + "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/ops" "github.com/stretchr/testify/assert" "go.uber.org/mock/gomock" @@ -43,15 +49,26 @@ func setup(t *testing.T) (*gomock.Controller, *mocks.MockGraph, *dbMocks.MockDat mockCtrl = gomock.NewController(t) mockGraph = mocks.NewMockGraph(mockCtrl) mockDB = dbMocks.NewMockDatabase(mockCtrl) - resources = v2.Resources{GraphQuery: mockGraph, DB: mockDB} + resources = v2.Resources{GraphQuery: mockGraph, DB: mockDB, DogTags: dogtags.NewTestService(dogtags.TestOverrides{})} ) return mockCtrl, mockGraph, mockDB, resources } func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apitest.Case { + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: model.User{}, + Session: model.UserSession{}, + }, + } + return []apitest.Case{ { Name: "RepoGetEntityQueryParamsError", + Input: func(input *apitest.Input) { + apitest.SetContext(input, bheCtx.ConstructGoContext()) + }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusBadRequest) apitest.BodyContains(output, "no object ID found in request") @@ -61,6 +78,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Name: "GraphDBGetADEntityQueryResultGraphUnsupportedError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -79,6 +97,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Name: "GraphDBGetADEntityQueryResultUnsupportedDataTypeError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -97,6 +116,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Name: "GraphDBGetADEntityQueryResultMemoryLimitError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -115,6 +135,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Name: "GraphDBGetADEntityQueryResultUnexpectedError", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -134,6 +155,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") apitest.AddQueryParam(input, "type", "graph") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -145,8 +167,8 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusOK) - //This flat unnested shape maintains the current api contract for a type=graph query - //Assert that the response does not contain pagination properties + // This flat unnested shape maintains the current api contract for a type=graph query + // Assert that the response does not contain pagination properties apitest.BodyNotContains(output, "data") apitest.BodyNotContains(output, "skip") apitest.BodyNotContains(output, "limit") @@ -157,8 +179,9 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit Name: "Success", Input: func(input *apitest.Input) { apitest.SetURLVar(input, "object_id", "1") - //Delete the type=graph param so that we get list results + // Delete the type=graph param so that we get list results apitest.DeleteQueryParam(input, "type") + apitest.SetContext(input, bheCtx.ConstructGoContext()) }, Setup: func() { mockGraph.EXPECT(). @@ -170,7 +193,7 @@ func setupCases(mockGraph *mocks.MockGraph, mockDB *dbMocks.MockDatabase) []apit }, Test: func(output apitest.Output) { apitest.StatusCode(output, http.StatusOK) - //List results are nested under "data" and the response contains other pagination properties + // List results are nested under "data" and the response contains other pagination properties apitest.BodyContains(output, "data") apitest.BodyContains(output, "skip") apitest.BodyContains(output, "limit") @@ -553,10 +576,12 @@ func TestResources_ListADIssuancePolicyLinkedCertTemplates(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -691,6 +716,125 @@ func TestResources_ListADIssuancePolicyLinkedCertTemplates(t *testing.T) { responseHeader: http.Header{"Content-Type": []string{"application/json"}}, }, }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id/linkedtemplates", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockDatabase.EXPECT().GetFlagByKey(gomock.Any(), "entity_panel_cache").Return(appcfg.FeatureFlag{Enabled: true}, nil) + mock.mockGraphQuery.EXPECT().GetADEntityQueryResult(gomock.Any(), gomock.Any(), true).Return("results", 1, nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"count":1,"limit":10,"skip":0,"data":"results"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id/linkedtemplates", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Entity).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + mock.mockDatabase.EXPECT().GetFlagByKey(gomock.Any(), "entity_panel_cache").Return(appcfg.FeatureFlag{Enabled: true}, nil) + mock.mockGraphQuery.EXPECT().GetADEntityQueryResult(gomock.Any(), gomock.Any(), true).Return("results", 1, nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"count":1,"limit":10,"skip":0,"data":"results"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/issuancepolicies/id/linkedtemplates", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", ad.Entity).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{ad.Entity}, + Properties: graph.AsProperties(map[string]any{ + "domainsid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, + }, } for _, testCase := range tt { t.Run(testCase.name, func(t *testing.T) { @@ -703,18 +847,28 @@ func TestResources_ListADIssuancePolicyLinkedCertTemplates(t *testing.T) { } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ DB: mocks.mockDatabase, GraphQuery: mocks.mockGraphQuery, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc(fmt.Sprintf("/api/v2/issuancepolicies/{%s}/linkedtemplates", api.URIPathVariableObjectID), resources.ListADIssuancePolicyLinkedCertTemplates).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc(fmt.Sprintf("/api/v2/issuancepolicies/{%s}/linkedtemplates", api.URIPathVariableObjectID), resources.ListADIssuancePolicyLinkedCertTemplates).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) diff --git a/cmd/api/src/api/v2/azure.go b/cmd/api/src/api/v2/azure.go index 7913ff7acd5..40415ee58b9 100644 --- a/cmd/api/src/api/v2/azure.go +++ b/cmd/api/src/api/v2/azure.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "log/slog" "net/http" "sort" @@ -27,9 +28,13 @@ import ( azure2 "github.com/specterops/bloodhound/cmd/api/src/analysis/azure" "github.com/specterops/bloodhound/cmd/api/src/api" "github.com/specterops/bloodhound/cmd/api/src/api/bloodhoundgraph" + "github.com/specterops/bloodhound/cmd/api/src/auth" + bhCtx "github.com/specterops/bloodhound/cmd/api/src/ctx" "github.com/specterops/bloodhound/cmd/api/src/model" "github.com/specterops/bloodhound/cmd/api/src/utils" "github.com/specterops/bloodhound/packages/go/analysis/azure" + "github.com/specterops/bloodhound/packages/go/bhlog/attr" + azure_schema "github.com/specterops/bloodhound/packages/go/graphschema/azure" "github.com/specterops/dawgs/graph" "github.com/specterops/dawgs/ops" ) @@ -199,7 +204,7 @@ func listRelatedEntityType(ctx context.Context, db graph.Database, entityType, o nodeSet graph.NodeSet err error ) - //NOTE: All skip/limit passed to lower level queries is currently hardcoded to 0 so we can get the full count of the dataset for skip/limit tracking + // NOTE: All skip/limit passed to lower level queries is currently hardcoded to 0 so we can get the full count of the dataset for skip/limit tracking switch relatedEntityType := azure.RelatedEntityType(entityType); relatedEntityType { case azure.RelatedEntityTypeDescendentUsers, azure.RelatedEntityTypeDescendentGroups, azure.RelatedEntityTypeDescendentManagementGroups, azure.RelatedEntityTypeDescendentSubscriptions, @@ -411,8 +416,25 @@ func (s *Resources) GetAZEntity(response http.ResponseWriter, request *http.Requ entityType = requestVars[entityTypePathParameterName] ) + user, isUser := auth.GetUserFromAuthCtx(bhCtx.FromRequest(request).AuthCtx) + if !isUser { + slog.Error("Unable to get user from auth context") + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + return + } + if objectID := queryVars.Get(objectIDQueryParameterName); objectID == "" { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("query parameter %s is required", objectIDQueryParameterName), request), response) + } else if azKind, err := azEntityParamToKind(entityType); err != nil { + slog.WarnContext(request.Context(), "Could not determine AZ type from entityType request var", + slog.String("entityType", entityType), + attr.Error(err)) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsBadQueryParameterFilters, request), response) + } else if hasAccess, err := CheckUserHasAccessToNodeById(request.Context(), s.DB, s.GraphQuery, s.DogTags, user, objectID, azKind); err != nil { + slog.ErrorContext(request.Context(), "Error checking if user has access to node for ETAC", attr.Error(err)) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + } else if !hasAccess { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, api.ErrorResponseDetailsForbidden, request), response) } else if relatedEntityTypeStr := queryVars.Get(relatedEntityTypeQueryParameterName); relatedEntityTypeStr != "" { s.GetAZRelatedEntities(request.Context(), response, request, objectID) } else if includeCounts, err := api.ParseOptionalBool(queryVars.Get(api.QueryParameterIncludeCounts), true); err != nil { @@ -427,3 +449,71 @@ func (s *Resources) GetAZEntity(response http.ResponseWriter, request *http.Requ api.WriteBasicResponse(request.Context(), entityInformation, http.StatusOK, response) } } + +// azEntityParamToKind takes a string which is parsed from a user's request params and converts it to a known Azure kind +// For example: `az-base` becomes the Kind `AZBase` +func azEntityParamToKind(entityType string) (graph.Kind, error) { + switch entityType { + case entityTypeBase: + return azure_schema.Entity, nil + case entityTypeUsers: + return azure_schema.User, nil + + case entityTypeGroups: + return azure_schema.Group, nil + + case entityTypeTenants: + return azure_schema.Tenant, nil + + case entityTypeManagementGroups: + return azure_schema.ManagementGroup, nil + + case entityTypeSubscriptions: + return azure_schema.Subscription, nil + + case entityTypeResourceGroups: + return azure_schema.ResourceGroup, nil + + case entityTypeVMs: + return azure_schema.VM, nil + + case entityTypeManagedClusters: + return azure_schema.ManagedCluster, nil + + case entityTypeContainerRegistries: + return azure_schema.ContainerRegistry, nil + + case entityTypeWebApps: + return azure_schema.WebApp, nil + + case entityTypeLogicApps: + return azure_schema.LogicApp, nil + + case entityTypeAutomationAccounts: + return azure_schema.AutomationAccount, nil + + case entityTypeKeyVaults: + return azure_schema.KeyVault, nil + + case entityTypeDevices: + return azure_schema.Device, nil + + case entityTypeApplications: + return azure_schema.App, nil + + case entityTypeVMScaleSets: + return azure_schema.VMScaleSet, nil + + case entityTypeServicePrincipals: + return azure_schema.ServicePrincipal, nil + + case entityTypeRoles: + return azure_schema.Role, nil + + case entityTypeFunctionApps: + return azure_schema.FunctionApp, nil + + default: + return nil, fmt.Errorf("unknown azure entity %s", entityType) + } +} diff --git a/cmd/api/src/api/v2/azure_test.go b/cmd/api/src/api/v2/azure_test.go index 5c85418e525..d5832d5b51a 100644 --- a/cmd/api/src/api/v2/azure_test.go +++ b/cmd/api/src/api/v2/azure_test.go @@ -25,7 +25,14 @@ import ( "testing" "github.com/gorilla/mux" + "github.com/specterops/bloodhound/cmd/api/src/auth" + "github.com/specterops/bloodhound/cmd/api/src/ctx" + mocks_db "github.com/specterops/bloodhound/cmd/api/src/database/mocks" + "github.com/specterops/bloodhound/cmd/api/src/model" + mocks_graph "github.com/specterops/bloodhound/cmd/api/src/queries/mocks" + "github.com/specterops/bloodhound/cmd/api/src/services/dogtags" "github.com/specterops/bloodhound/packages/go/analysis/azure" + azure_schema "github.com/specterops/bloodhound/packages/go/graphschema/azure" graphmocks "github.com/specterops/bloodhound/cmd/api/src/vendormocks/dawgs/graph" "github.com/specterops/dawgs/graph" @@ -42,7 +49,9 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { t.Parallel() type mock struct { - mockDB *graphmocks.MockDatabase + mockDatabase *mocks_db.MockDatabase + mockGraphDB *graphmocks.MockDatabase + mockGraphQuery *mocks_graph.MockGraph } type expected struct { responseBody string @@ -50,10 +59,12 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -81,7 +92,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/invalid", + Path: "/api/v2/azure/roles", RawQuery: "type=bad&object_id=id&related_entity_type=list", }, Method: http.MethodGet, @@ -99,7 +110,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/type", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=list&skip=true", }, Method: http.MethodGet, @@ -117,7 +128,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/type", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=list&skip=1&limit=true", }, Method: http.MethodGet, @@ -135,7 +146,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/type", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&type=graph&skip=0&limit=1&related_entity_type=inbound-control", }, Method: http.MethodGet, @@ -143,7 +154,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterSkip) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterSkip) }, expected: expected{ responseCode: http.StatusInternalServerError, @@ -156,7 +167,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&type=graph&skip=0&limit=1&related_entity_type=inbound-control", }, Method: http.MethodGet, @@ -164,7 +175,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, expected: expected{ responseCode: http.StatusOK, @@ -177,7 +188,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=descendent-users&skip=0&limit=1", }, Method: http.MethodGet, @@ -185,7 +196,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterSkip) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterSkip) }, expected: expected{ responseCode: http.StatusBadRequest, @@ -198,7 +209,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=descendent-users&skip=0&limit=1", }, Method: http.MethodGet, @@ -206,7 +217,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterRelatedEntityType) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(v2.ErrParameterRelatedEntityType) }, expected: expected{ responseCode: http.StatusNotFound, @@ -219,7 +230,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=descendent-users&skip=0&limit=1", }, Method: http.MethodGet, @@ -227,7 +238,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(ops.ErrGraphQueryMemoryLimit) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(ops.ErrGraphQueryMemoryLimit) }, expected: expected{ responseCode: http.StatusInternalServerError, @@ -240,7 +251,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=descendent-users&skip=0&limit=1", }, Method: http.MethodGet, @@ -248,7 +259,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, expected: expected{ responseCode: http.StatusInternalServerError, @@ -261,7 +272,7 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=inbound-control&skip=0&limit=1", }, Method: http.MethodGet, @@ -269,13 +280,136 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"count":0,"limit":1,"skip":0,"data":[]}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&related_entity_type=inbound-control&skip=0&limit=1", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, expected: expected{ responseCode: http.StatusOK, responseBody: `{"count":0,"limit":1,"skip":0,"data":[]}`, responseHeader: http.Header{"Content-Type": []string{"application/json"}}, }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&related_entity_type=inbound-control&skip=0&limit=1", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + + props := graph.AsProperties(map[string]any{ + "tenantid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", azure_schema.Role).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{azure_schema.Entity, azure_schema.Role}, + Properties: props, + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"count":0,"limit":1,"skip":0,"data":[]}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&related_entity_type=inbound-control&skip=0&limit=1", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", azure_schema.Role).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{azure_schema.Entity, azure_schema.Role}, + Properties: graph.AsProperties(map[string]any{ + "tenantid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, }, } for _, testCase := range tt { @@ -284,21 +418,35 @@ func TestResources_GetAZRelatedEntities(t *testing.T) { ctrl := gomock.NewController(t) mocks := &mock{ - mockDB: graphmocks.NewMockDatabase(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), + mockGraphDB: graphmocks.NewMockDatabase(ctrl), + mockGraphQuery: mocks_graph.NewMockGraph(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ - Graph: mocks.mockDB, + Graph: mocks.mockGraphDB, + GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc("/api/v2/azure/{entity_type}", resources.GetAZEntity).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc("/api/v2/azure/{entity_type}", resources.GetAZEntity).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) @@ -313,7 +461,9 @@ func TestResources_GetAZEntityInformation(t *testing.T) { t.Parallel() type mock struct { - mockDB *graphmocks.MockDatabase + mockDatabase *mocks_db.MockDatabase + mockGraphDB *graphmocks.MockDatabase + mockGraphQuery *mocks_graph.MockGraph } type args struct { entityType string @@ -337,7 +487,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -351,7 +501,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.BaseDetails(azure.BaseDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, OutboundObjectControl: 0}), @@ -365,7 +515,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -379,7 +529,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.UserDetails(azure.UserDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, GroupMembership: 0, Roles: 0, ExecutionPrivileges: 0, OutboundObjectControl: 0, InboundObjectControl: 0}), @@ -393,7 +543,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -407,7 +557,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.GroupDetails(azure.GroupDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Roles: 0, GroupMembers: 0, GroupMembership: 0, OutboundObjectControl: 0, InboundObjectControl: 0}), @@ -421,7 +571,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -435,7 +585,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.TenantDetails(azure.TenantDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Descendents: azure.Descendents{DescendentCounts: map[string]int(nil)}, InboundObjectControl: 0}), @@ -449,7 +599,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -463,7 +613,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ManagementGroupDetails(azure.ManagementGroupDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Descendents: azure.Descendents{DescendentCounts: map[string]int(nil)}, InboundObjectControl: 0}), @@ -477,7 +627,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -491,7 +641,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.SubscriptionDetails(azure.SubscriptionDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Descendents: azure.Descendents{DescendentCounts: map[string]int(nil)}, InboundObjectControl: 0}), @@ -505,7 +655,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -519,7 +669,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ResourceGroupDetails(azure.ResourceGroupDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Descendents: azure.Descendents{DescendentCounts: map[string]int(nil)}, InboundObjectControl: 0}), @@ -533,7 +683,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -547,7 +697,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.VMDetails(azure.VMDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundExecutionPrivileges: 0, InboundObjectControl: 0}), @@ -561,7 +711,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -575,7 +725,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ManagedClusterDetails(azure.ManagedClusterDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -589,7 +739,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -603,7 +753,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ContainerRegistryDetails(azure.ContainerRegistryDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -617,7 +767,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -631,7 +781,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.WebAppDetails(azure.WebAppDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -645,7 +795,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -659,7 +809,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.LogicAppDetails(azure.LogicAppDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -673,7 +823,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -687,7 +837,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.AutomationAccountDetails(azure.AutomationAccountDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -701,7 +851,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -715,7 +865,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.KeyVaultDetails(azure.KeyVaultDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Readers: azure.KeyVaultReaderCounts{KeyReaders: 0, CertificateReaders: 0, SecretReaders: 0, AllReaders: 0}, InboundObjectControl: 0}), @@ -729,7 +879,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -743,7 +893,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.DeviceDetails(azure.DeviceDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundExecutionPrivileges: 0, InboundObjectControl: 0}), @@ -757,7 +907,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -771,7 +921,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ApplicationDetails(azure.ApplicationDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -785,7 +935,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -799,7 +949,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.VMScaleSetDetails(azure.VMScaleSetDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -813,7 +963,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -827,7 +977,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.ServicePrincipalDetails(azure.ServicePrincipalDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, Roles: 0, InboundObjectControl: 0, OutboundObjectControl: 0, InboundAbusableAppRoleAssignments: 0, OutboundAbusableAppRoleAssignments: 0}), @@ -841,7 +991,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -855,7 +1005,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.RoleDetails(azure.RoleDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, ActiveAssignments: 0, PIMAssignments: 0}), @@ -869,7 +1019,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(errors.New("error")) }, want: want{ res: nil, @@ -883,7 +1033,7 @@ func TestResources_GetAZEntityInformation(t *testing.T) { }, setupMocks: func(t *testing.T, mocks *mock) { t.Helper() - mocks.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mocks.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, want: want{ res: azure.FunctionAppDetails(azure.FunctionAppDetails{Node: azure.Node{Kind: "", Properties: map[string]interface{}(nil)}, InboundObjectControl: 0}), @@ -908,12 +1058,14 @@ func TestResources_GetAZEntityInformation(t *testing.T) { ctrl := gomock.NewController(t) mocks := &mock{ - mockDB: graphmocks.NewMockDatabase(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), + mockGraphDB: graphmocks.NewMockDatabase(ctrl), + mockGraphQuery: mocks_graph.NewMockGraph(ctrl), } testCase.setupMocks(t, mocks) - res, err := v2.GetAZEntityInformation(context.Background(), mocks.mockDB, testCase.args.entityType, "id", false) + res, err := v2.GetAZEntityInformation(context.Background(), mocks.mockGraphDB, testCase.args.entityType, "id", false) if err != nil && testCase.want.err != nil { require.Equal(t, testCase.want.err, err) @@ -928,7 +1080,9 @@ func TestManagementResource_GetAZEntity(t *testing.T) { t.Parallel() type mock struct { - mockDB *graphmocks.MockDatabase + mockDatabase *mocks_db.MockDatabase + mockGraphDB *graphmocks.MockDatabase + mockGraphQuery *mocks_graph.MockGraph } type expected struct { responseBody string @@ -936,10 +1090,12 @@ func TestManagementResource_GetAZEntity(t *testing.T) { responseHeader http.Header } type testData struct { - name string - buildRequest func() *http.Request - setupMocks func(t *testing.T, mock *mock) - expected expected + name string + buildRequest func() *http.Request + setupMocks func(t *testing.T, mock *mock) + user model.User + dogTagsOverrides dogtags.TestOverrides + expected expected } tt := []testData{ @@ -966,7 +1122,7 @@ func TestManagementResource_GetAZEntity(t *testing.T) { buildRequest: func() *http.Request { return &http.Request{ URL: &url.URL{ - Path: "/api/v2/azure/{entity_type}", + Path: "/api/v2/azure/roles", RawQuery: "object_id=id&related_entity_type=bad", }, Method: http.MethodGet, @@ -1010,7 +1166,7 @@ func TestManagementResource_GetAZEntity(t *testing.T) { }, setupMocks: func(t *testing.T, mock *mock) { t.Helper() - mock.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(graph.ErrNoResultsFound) + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(graph.ErrNoResultsFound) }, expected: expected{ responseCode: http.StatusNotFound, @@ -1031,8 +1187,8 @@ func TestManagementResource_GetAZEntity(t *testing.T) { }, setupMocks: func(t *testing.T, mock *mock) {}, expected: expected{ - responseCode: http.StatusInternalServerError, - responseBody: `{"errors":[{"context":"","message":"db error: unknown azure entity unknown"}],"http_status":500,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseCode: http.StatusBadRequest, + responseBody: `{"errors":[{"context":"","message":"there are errors in the query parameter filters specified"}],"http_status":400,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, responseHeader: http.Header{"Content-Type": []string{"application/json"}}, }, }, @@ -1049,13 +1205,135 @@ func TestManagementResource_GetAZEntity(t *testing.T) { }, setupMocks: func(t *testing.T, mock *mock) { t.Helper() - mock.mockDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"isOwnedObject":false, "isTierZero":false, "kind":"","props":null,"active_assignments":0,"approvers":0, "kinds":null, "pim_assignments":0}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + }, + { + name: "Success: ETAC enabled AllEnvironments", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) }, expected: expected{ responseCode: http.StatusOK, responseBody: `{"data":{"isOwnedObject":false, "isTierZero":false, "kind":"","props":null,"active_assignments":0,"approvers":0, "kinds":null, "pim_assignments":0}}`, responseHeader: http.Header{"Content-Type": []string{"application/json"}}, }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: true, + }, + }, + { + name: "Success: ETAC enabled For Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphDB.EXPECT().ReadTransaction(gomock.Any(), gomock.Any()).Return(nil) + + props := graph.AsProperties(map[string]any{ + "tenantid": "12345", + }) + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", azure_schema.Role).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{azure_schema.Entity, azure_schema.Role}, + Properties: props, + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusOK, + responseBody: `{"data":{"isOwnedObject":false, "isTierZero":false, "kind":"","props":null,"active_assignments":0,"approvers":0, "kinds":null, "pim_assignments":0}}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "12345", + }, + }, + }, + }, + { + name: "Error: ETAC User Does Not have Access To Specific Environment", + buildRequest: func() *http.Request { + return &http.Request{ + URL: &url.URL{ + Path: "/api/v2/azure/roles", + RawQuery: "object_id=id&counts=true", + }, + Method: http.MethodGet, + } + }, + setupMocks: func(t *testing.T, mock *mock) { + t.Helper() + mock.mockGraphQuery.EXPECT().GetEntityByObjectId(gomock.Any(), "id", azure_schema.Role).Return(&graph.Node{ + ID: graph.ID(16), + Kinds: graph.Kinds{azure_schema.Entity, azure_schema.Role}, + Properties: graph.AsProperties(map[string]any{ + "tenantid": "12345", + }), + }, nil) + mock.mockDatabase.EXPECT().GetEnvironmentTargetedAccessControlForUser(gomock.Any(), gomock.Any()).Return([]model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, nil) + }, + expected: expected{ + responseCode: http.StatusForbidden, + responseBody: `{"errors":[{"context":"","message":"Forbidden"}],"http_status":403,"request_id":"","timestamp":"0001-01-01T00:00:00Z"}`, + responseHeader: http.Header{"Content-Type": []string{"application/json"}}, + }, + dogTagsOverrides: dogtags.TestOverrides{ + Bools: map[dogtags.BoolDogTag]bool{ + dogtags.ETAC_ENABLED: true, + }, + }, + user: model.User{ + AllEnvironments: false, + EnvironmentTargetedAccessControl: []model.EnvironmentTargetedAccessControl{ + { + EnvironmentID: "54321", + }, + }, + }, }, } for _, testCase := range tt { @@ -1064,21 +1342,35 @@ func TestManagementResource_GetAZEntity(t *testing.T) { ctrl := gomock.NewController(t) mocks := &mock{ - mockDB: graphmocks.NewMockDatabase(ctrl), + mockDatabase: mocks_db.NewMockDatabase(ctrl), + mockGraphDB: graphmocks.NewMockDatabase(ctrl), + mockGraphQuery: mocks_graph.NewMockGraph(ctrl), } request := testCase.buildRequest() + bheCtx := ctx.Context{ + AuthCtx: auth.Context{ + PermissionOverrides: auth.PermissionOverrides{}, + Owner: testCase.user, + Session: model.UserSession{}, + }, + } + requestWithCtx := request.WithContext(bheCtx.ConstructGoContext()) + testCase.setupMocks(t, mocks) resources := v2.Resources{ - Graph: mocks.mockDB, + Graph: mocks.mockGraphDB, + GraphQuery: mocks.mockGraphQuery, + DB: mocks.mockDatabase, + DogTags: dogtags.NewTestService(testCase.dogTagsOverrides), } response := httptest.NewRecorder() router := mux.NewRouter() - router.HandleFunc("/api/v2/azure/{entity_type}", resources.GetAZEntity).Methods(request.Method) - router.ServeHTTP(response, request) + router.HandleFunc("/api/v2/azure/{entity_type}", resources.GetAZEntity).Methods(requestWithCtx.Method) + router.ServeHTTP(response, requestWithCtx) status, header, body := test.ProcessResponse(t, response) diff --git a/cmd/api/src/api/v2/etac.go b/cmd/api/src/api/v2/etac.go index 34ed4cbaef0..75ed9b72c95 100644 --- a/cmd/api/src/api/v2/etac.go +++ b/cmd/api/src/api/v2/etac.go @@ -24,7 +24,10 @@ import ( "github.com/specterops/bloodhound/cmd/api/src/database" "github.com/specterops/bloodhound/cmd/api/src/model" + "github.com/specterops/bloodhound/cmd/api/src/queries" "github.com/specterops/bloodhound/cmd/api/src/services/dogtags" + "github.com/specterops/bloodhound/packages/go/graphschema/ad" + "github.com/specterops/bloodhound/packages/go/graphschema/azure" "github.com/specterops/dawgs/graph" ) @@ -63,6 +66,43 @@ func CheckUserAccessToEnvironments(ctx context.Context, db database.EnvironmentT return true, nil } +// CheckUserHasAccessToNodeById returns whether a user has access to view this node based on their ETAC list +func CheckUserHasAccessToNodeById(ctx context.Context, db database.Database, graphQuery queries.Graph, dogTagsService dogtags.Service, user model.User, objectId string, kind graph.Kind) (bool, error) { + if ShouldFilterForETAC(dogTagsService, user) { + node, err := graphQuery.GetEntityByObjectId(ctx, objectId, kind) + if err != nil || node == nil { + return false, err + } + + var environmentId string + if node.Kinds.ContainsOneOf(azure.Entity) { + if tenantId, err := node.Properties.Get(azure.TenantID.String()).String(); err != nil { + return false, err + } else { + environmentId = tenantId + } + } else if node.Kinds.ContainsOneOf(ad.Entity) { + if domainSid, err := node.Properties.Get(ad.DomainSID.String()).String(); err != nil { + return false, err + } else { + environmentId = domainSid + } + } + + if environmentId == "" { + return false, fmt.Errorf("could not find environment id for %s", objectId) + } + + if hasAccess, err := CheckUserAccessToEnvironments(ctx, db, user, environmentId); err != nil { + return false, err + } else if !hasAccess { + return false, nil + } + } + + return true, nil +} + // ExtractEnvironmentIDsFromUser is a helper function // to extract a user's environments from their model as a list of strings func ExtractEnvironmentIDsFromUser(user *model.User) []string {