Skip to content

Commit

Permalink
Merge branch 'main' into BED-4965
Browse files Browse the repository at this point in the history
  • Loading branch information
Mayyhem committed Dec 2, 2024
2 parents 0ad719d + 00a246f commit 5b31607
Show file tree
Hide file tree
Showing 66 changed files with 1,822 additions and 518 deletions.
Binary file not shown.
1 change: 1 addition & 0 deletions cmd/api/src/api/constant.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ const (
URIPathVariableAssetGroupSelectorID = "asset_group_selector_id"
URIPathVariableAttackPathID = "attack_path_id"
URIPathVariableClientID = "client_id"
URIPathVariableDataType = "data_type"
URIPathVariableDomainID = "domain_id"
URIPathVariableEventID = "event_id"
URIPathVariableFeatureID = "feature_id"
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/api/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const (
ErrorResponseDetailsOTPInvalid = "one time password is invalid"
ErrorResponseDetailsResourceNotFound = "resource not found"
ErrorResponseDetailsToBeforeFrom = "to time cannot be before from time"
ErrorResponseDetailsTimeRangeInvalid = "time range provided is invalid"
ErrorResponseDetailsToMalformed = "to parameter should be formatted as RFC3339 i.e 2021-04-21T07:20:50.52Z"
ErrorResponseMultipleCollectionScopesProvided = "may only scope collection by exactly one of OU, Domain, or All Trusted Domains"
ErrorResponsePayloadUnmarshalError = "error unmarshalling JSON payload"
Expand Down
5 changes: 4 additions & 1 deletion cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ func registerV2Auth(resources v2.Resources, routerInst *router.Router, permissio
routerInst.GET("/api/v2/sso-providers", managementResource.ListAuthProviders),
routerInst.POST("/api/v2/sso-providers/oidc", managementResource.CreateOIDCProvider).CheckFeatureFlag(resources.DB, appcfg.FeatureOIDCSupport).RequirePermissions(permissions.AuthManageProviders),
routerInst.DELETE(fmt.Sprintf("/api/v2/sso-providers/{%s}", api.URIPathVariableSSOProviderID), managementResource.DeleteSSOProvider).RequirePermissions(permissions.AuthManageProviders),
routerInst.PATCH(fmt.Sprintf("/api/v2/sso-providers/{%s}", api.URIPathVariableSSOProviderID), managementResource.UpdateSSOProvider).RequirePermissions(permissions.AuthManageProviders),
routerInst.GET(fmt.Sprintf("/api/v2/sso-providers/{%s}/signing-certificate", api.URIPathVariableSSOProviderID), managementResource.ServeSigningCertificate).RequirePermissions(permissions.AuthManageProviders),

routerInst.GET(fmt.Sprintf("/api/v2/sso/{%s}/login", api.URIPathVariableSSOProviderSlug), managementResource.SSOLoginHandler),
routerInst.GET(fmt.Sprintf("/api/v2/sso/{%s}/metadata", api.URIPathVariableSSOProviderSlug), managementResource.ServeMetadata),
routerInst.PathPrefix(fmt.Sprintf("/api/v2/sso/{%s}/callback", api.URIPathVariableSSOProviderSlug), http.HandlerFunc(managementResource.SSOCallbackHandler)),
routerInst.GET(fmt.Sprintf("/api/v2/sso/{%s}/metadata", api.URIPathVariableSSOProviderSlug), managementResource.ServeMetadata),

// Permissions
routerInst.GET("/api/v2/permissions", managementResource.ListPermissions).RequirePermissions(permissions.AuthManageSelf),
Expand Down
4 changes: 1 addition & 3 deletions cmd/api/src/api/v2/analysisrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ package v2

import (
"database/sql"
"errors"
"net/http"

"github.com/specterops/bloodhound/errors"
"github.com/specterops/bloodhound/log"
"github.com/specterops/bloodhound/src/api"
"github.com/specterops/bloodhound/src/auth"
Expand All @@ -33,8 +33,6 @@ const ErrAnalysisScheduledMode = "analysis is configured to run on a schedule, u
func (s Resources) GetAnalysisRequest(response http.ResponseWriter, request *http.Request) {
if analRequest, err := s.DB.GetAnalysisRequest(request.Context()); err != nil && !errors.Is(err, sql.ErrNoRows) {
api.HandleDatabaseError(request, response, err)
} else if errors.Is(err, sql.ErrNoRows) {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response)
} else {
api.WriteBasicResponse(request.Context(), analRequest, http.StatusOK, response)
}
Expand Down
39 changes: 39 additions & 0 deletions cmd/api/src/api/v2/analysisrequest_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright 2024 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//go:build integration
// +build integration

package v2_test

import (
"testing"

"github.com/specterops/bloodhound/src/api/v2/integration"
"github.com/specterops/bloodhound/src/model"
"github.com/stretchr/testify/require"
)

func TestRequestAnalysis(t *testing.T) {
testCtx := integration.NewFOSSContext(t)

err := testCtx.AdminClient().RequestAnalysis()
require.Nil(t, err)

analReq, err := testCtx.AdminClient().GetAnalysisRequest()
require.Nil(t, err)
require.Equal(t, analReq.RequestType, model.AnalysisRequestAnalysis)
}
57 changes: 45 additions & 12 deletions cmd/api/src/api/v2/analysisrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,59 @@
//
// SPDX-License-Identifier: Apache-2.0

//go:build integration
// +build integration

package v2_test

import (
"fmt"
"net/http"
"testing"
"time"

"github.com/specterops/bloodhound/src/api/v2/integration"
v2 "github.com/specterops/bloodhound/src/api/v2"
dbMocks "github.com/specterops/bloodhound/src/database/mocks"
"github.com/specterops/bloodhound/src/model"
"github.com/stretchr/testify/require"
"github.com/specterops/bloodhound/src/utils/test"
"go.uber.org/mock/gomock"
)

func TestRequestAnalysis(t *testing.T) {
testCtx := integration.NewFOSSContext(t)
func TestResources_GetAnalysisRequest(t *testing.T) {
const (
url = "api/v2/analysis/status"
)

var (
mockCtrl = gomock.NewController(t)
mockDB = dbMocks.NewMockDatabase(mockCtrl)
resources = v2.Resources{DB: mockDB}
)
defer mockCtrl.Finish()

t.Run("success getting analysis", func(t *testing.T) {
analysisRequest := model.AnalysisRequest{
RequestedAt: time.Now(),
RequestedBy: "test",
RequestType: model.AnalysisRequestType("test-type"),
}

mockDB.EXPECT().GetAnalysisRequest(gomock.Any()).Return(analysisRequest, nil)

test.Request(t).
WithMethod(http.MethodGet).
WithURL(url).
OnHandlerFunc(resources.GetAnalysisRequest).
Require().
ResponseJSONBody(analysisRequest).
ResponseStatusCode(http.StatusOK)
})

err := testCtx.AdminClient().RequestAnalysis()
require.Nil(t, err)
t.Run("error getting analysis", func(t *testing.T) {
mockDB.EXPECT().GetAnalysisRequest(gomock.Any()).Return(model.AnalysisRequest{}, fmt.Errorf("an error"))

analReq, err := testCtx.AdminClient().GetAnalysisRequest()
require.Nil(t, err)
require.Equal(t, analReq.RequestType, model.AnalysisRequestAnalysis)
test.Request(t).
WithMethod(http.MethodGet).
WithURL(url).
OnHandlerFunc(resources.GetAnalysisRequest).
Require().
ResponseStatusCode(http.StatusInternalServerError)
})
}
1 change: 1 addition & 0 deletions cmd/api/src/api/v2/attack_path.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
ErrorNoFindingType = "no finding type specified"
ErrorInvalidFindingType = "invalid finding type specified: %v"
ErrorInvalidRFC3339 = "invalid RFC-3339 datetime format: %v"
ErrorNoDataType = "no data type specified in url"
)

type RiskAcceptRequest struct {
Expand Down
48 changes: 40 additions & 8 deletions cmd/api/src/api/v2/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,57 @@ import (
"golang.org/x/oauth2"
)

// CreateOIDCProviderRequest represents the body of the CreateOIDCProvider endpoint
type CreateOIDCProviderRequest struct {
// UpsertOIDCProviderRequest represents the body of create & update provider endpoints
type UpsertOIDCProviderRequest struct {
Name string `json:"name" validate:"required"`
Issuer string `json:"issuer" validate:"url"`
ClientID string `json:"client_id" validate:"required"`
}

// UpdateOIDCProviderRequest updates an OIDC provider, support for only partial payloads
func (s ManagementResource) UpdateOIDCProviderRequest(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) {
var upsertReq UpsertOIDCProviderRequest

if ssoProvider.OIDCProvider == nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response)
} else if err := api.ReadJSONRequestPayloadLimited(&upsertReq, request); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
} else {
if upsertReq.Name != "" {
ssoProvider.Name = upsertReq.Name
}

if upsertReq.ClientID != "" {
ssoProvider.OIDCProvider.ClientID = upsertReq.ClientID
}

if upsertReq.Issuer != "" {
if err := validation.ValidUrl(upsertReq.Issuer); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "issuer url is invalid", request), response)
return
}

ssoProvider.OIDCProvider.Issuer = upsertReq.Issuer
}

if oidcProvider, err := s.db.UpdateOIDCProvider(request.Context(), ssoProvider); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusOK, response)
}
}
}

// CreateOIDCProvider creates an OIDC provider entry given a valid request
func (s ManagementResource) CreateOIDCProvider(response http.ResponseWriter, request *http.Request) {
var (
createRequest = CreateOIDCProviderRequest{}
)
var upsertReq UpsertOIDCProviderRequest

if err := api.ReadJSONRequestPayloadLimited(&createRequest, request); err != nil {
if err := api.ReadJSONRequestPayloadLimited(&upsertReq, request); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
} else if validated := validation.Validate(createRequest); validated != nil {
} else if validated := validation.Validate(upsertReq); validated != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, validated.Error(), request), response)
} else {
if oidcProvider, err := s.db.CreateOIDCProvider(request.Context(), createRequest.Name, createRequest.Issuer, createRequest.ClientID); err != nil {
if oidcProvider, err := s.db.CreateOIDCProvider(request.Context(), upsertReq.Name, upsertReq.Issuer, upsertReq.ClientID); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusCreated, response)
Expand Down
107 changes: 89 additions & 18 deletions cmd/api/src/api/v2/auth/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,13 @@ import (

"github.com/specterops/bloodhound/src/api/v2/apitest"
"github.com/specterops/bloodhound/src/api/v2/auth"
"github.com/specterops/bloodhound/src/database"
"github.com/specterops/bloodhound/src/model"
"github.com/specterops/bloodhound/src/utils/test"
"go.uber.org/mock/gomock"
)

func TestManagementResource_CreateOIDCProvider(t *testing.T) {
const (
url = "/api/v2/sso/providers/oidc"
)
var (
mockCtrl = gomock.NewController(t)
resources, mockDB = apitest.NewAuthManagementResource(mockCtrl)
Expand All @@ -45,9 +43,7 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
}, nil)

test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
WithBody(auth.UpsertOIDCProviderRequest{
Name: "Bloodhound gang",
Issuer: "https://localhost/auth",
ClientID: "bloodhound",
Expand All @@ -59,19 +55,14 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {

t.Run("error parsing body request", func(t *testing.T) {
test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody("").
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
ResponseStatusCode(http.StatusBadRequest)
})

t.Run("error validating request field", func(t *testing.T) {
test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
WithBody(auth.UpsertOIDCProviderRequest{
Name: "test",
Issuer: "1234:not:a:url",
ClientID: "bloodhound",
Expand All @@ -82,12 +73,10 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
})

t.Run("error invalid Issuer", func(t *testing.T) {
request := auth.CreateOIDCProviderRequest{
request := auth.UpsertOIDCProviderRequest{
Issuer: "12345:bloodhound",
}
test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(request).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
Expand All @@ -98,9 +87,7 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "test", "https://localhost/auth", "bloodhound").Return(model.OIDCProvider{}, fmt.Errorf("error"))

test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
WithBody(auth.UpsertOIDCProviderRequest{
Name: "test",
Issuer: "https://localhost/auth",
ClientID: "bloodhound",
Expand All @@ -110,3 +97,87 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
ResponseStatusCode(http.StatusInternalServerError)
})
}

func TestManagementResource_UpdateOIDCProvider(t *testing.T) {
var (
mockCtrl = gomock.NewController(t)
resources, mockDB = apitest.NewAuthManagementResource(mockCtrl)
baseProvider = model.SSOProvider{
Type: model.SessionAuthProviderOIDC,
Name: "Gotham Net",
OIDCProvider: &model.OIDCProvider{
ClientID: "gotham-net",
Issuer: "https://gotham.net",
},
}
urlParams = map[string]string{"sso_provider_id": "1"}
)
defer mockCtrl.Finish()

t.Run("successfully update an OIDCProvider", func(t *testing.T) {
mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil)
mockDB.EXPECT().UpdateOIDCProvider(gomock.Any(), gomock.Any())

test.Request(t).
WithURLPathVars(urlParams).
WithBody(auth.UpsertOIDCProviderRequest{
Name: "Gotham Net 2",
Issuer: "https://gotham-2.net",
ClientID: "gotham-net-2",
}).
OnHandlerFunc(resources.UpdateSSOProvider).
Require().
ResponseStatusCode(http.StatusOK)
})

t.Run("error not found while updating an unknown OIDCProvider", func(t *testing.T) {
mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(model.SSOProvider{}, database.ErrNotFound)

test.Request(t).
WithURLPathVars(urlParams).
OnHandlerFunc(resources.UpdateSSOProvider).
Require().
ResponseStatusCode(http.StatusNotFound)
})

t.Run("error parsing body request", func(t *testing.T) {
mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil)

test.Request(t).
WithURLPathVars(urlParams).
OnHandlerFunc(resources.UpdateSSOProvider).
Require().
ResponseStatusCode(http.StatusBadRequest)
})

t.Run("error validating request field", func(t *testing.T) {
mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil)

test.Request(t).
WithURLPathVars(urlParams).
WithBody(auth.UpsertOIDCProviderRequest{
Name: "test",
Issuer: "1234:not:a:url",
ClientID: "bloodhound",
}).
OnHandlerFunc(resources.UpdateSSOProvider).
Require().
ResponseStatusCode(http.StatusBadRequest)
})

t.Run("error creating oidc provider db entry", func(t *testing.T) {
mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil)
mockDB.EXPECT().UpdateOIDCProvider(gomock.Any(), gomock.Any()).Return(model.OIDCProvider{}, fmt.Errorf("error"))

test.Request(t).
WithURLPathVars(urlParams).
WithBody(auth.UpsertOIDCProviderRequest{
Name: "test",
Issuer: "https://localhost/auth",
ClientID: "bloodhound",
}).
OnHandlerFunc(resources.UpdateSSOProvider).
Require().
ResponseStatusCode(http.StatusInternalServerError)
})
}
Loading

0 comments on commit 5b31607

Please sign in to comment.