Skip to content

Commit

Permalink
BED-4851 Create OIDC Provider Endpoint (#881)
Browse files Browse the repository at this point in the history
* BED-4851 Create OIDC Provider Endpoint & migration
  • Loading branch information
mvlipka authored Sep 26, 2024
1 parent ab521ab commit 34201d2
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 16 deletions.
4 changes: 4 additions & 0 deletions cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/specterops/bloodhound/src/auth"
"github.com/specterops/bloodhound/src/config"
"github.com/specterops/bloodhound/src/database"
"github.com/specterops/bloodhound/src/model/appcfg"
)

func samlWriteAPIErrorResponse(request *http.Request, response http.ResponseWriter, statusCode int, message string) {
Expand Down Expand Up @@ -60,6 +61,9 @@ func registerV2Auth(cfg config.Configuration, db database.Database, permissions
routerInst.GET(fmt.Sprintf("/api/v2/saml/providers/{%s}", api.URIPathVariableSAMLProviderID), managementResource.GetSAMLProvider).RequirePermissions(permissions.AuthManageProviders),
routerInst.DELETE(fmt.Sprintf("/api/v2/saml/providers/{%s}", api.URIPathVariableSAMLProviderID), managementResource.DeleteSAMLProvider).RequirePermissions(permissions.AuthManageProviders),

// SSO
routerInst.POST("/api/v2/sso/providers/oidc", managementResource.CreateOIDCProvider).CheckFeatureFlag(db, appcfg.FeatureOIDCSupport).RequirePermissions(permissions.AuthManageProviders),

// Permissions
routerInst.GET("/api/v2/permissions", managementResource.ListPermissions).RequirePermissions(permissions.AuthManageSelf),
routerInst.GET(fmt.Sprintf("/api/v2/permissions/{%s}", api.URIPathVariablePermissionID), managementResource.GetPermission).RequirePermissions(permissions.AuthManageSelf),
Expand Down
58 changes: 58 additions & 0 deletions cmd/api/src/api/v2/auth/oidc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2024 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package auth

import (
"net/http"
"strings"

"github.com/specterops/bloodhound/src/utils/validation"

"github.com/specterops/bloodhound/src/api"
)

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

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

if err := api.ReadJSONRequestPayloadLimited(&createRequest, request); err != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response)
} else if validated := validation.Validate(createRequest); validated != nil {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, validated.Error(), request), response)
} else if strings.Contains(createRequest.Name, " ") {
api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "invalid name formatting, ensure there are no spaces in the provided name", request), response)
} else {
var (
formattedName = strings.ToLower(createRequest.Name)
)

if provider, err := s.db.CreateOIDCProvider(request.Context(), formattedName, createRequest.Issuer, createRequest.ClientID); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteBasicResponse(request.Context(), provider, http.StatusCreated, response)
}
}
}
118 changes: 118 additions & 0 deletions cmd/api/src/api/v2/auth/oidc_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// Copyright 2024 Specter Ops, Inc.
//
// Licensed under the Apache License, Version 2.0
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

package auth_test

import (
"fmt"
"net/http"

"github.com/specterops/bloodhound/src/model"

"github.com/specterops/bloodhound/src/api/v2/auth"

"github.com/specterops/bloodhound/src/api/v2/apitest"
"github.com/specterops/bloodhound/src/utils/test"

"testing"

"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)
)
defer mockCtrl.Finish()

t.Run("successfully create a new OIDCProvider", func(t *testing.T) {
mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "test", "https://localhost/auth", "bloodhound").Return(model.OIDCProvider{
Name: "",
ClientID: "",
Issuer: "",
}, nil)

test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
Name: "test",
Issuer: "https://localhost/auth",

ClientID: "bloodhound",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
ResponseStatusCode(http.StatusCreated)
})

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{
Name: "test",
Issuer: "",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
ResponseStatusCode(http.StatusBadRequest)
})

t.Run("error invalid Issuer", func(t *testing.T) {
request := auth.CreateOIDCProviderRequest{
Issuer: "12345:bloodhound",
}
test.Request(t).
WithMethod(http.MethodPost).
WithURL(url).
WithBody(request).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
ResponseStatusCode(http.StatusBadRequest)
})

t.Run("error creating oidc provider db entry", func(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{
Name: "test",
Issuer: "https://localhost/auth",

ClientID: "bloodhound",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
ResponseStatusCode(http.StatusInternalServerError)
})
}
3 changes: 3 additions & 0 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ type Database interface {
GetSAMLProviderUsers(ctx context.Context, id int32) (model.Users, error)
DeleteSAMLProvider(ctx context.Context, samlProvider model.SAMLProvider) error

// SSO
OIDCProviderData

// Sessions
CreateUserSession(ctx context.Context, userSession model.UserSession) (model.UserSession, error)
SetUserSessionFlag(ctx context.Context, userSession *model.UserSession, key model.SessionFlagKey, state bool) error
Expand Down
56 changes: 40 additions & 16 deletions cmd/api/src/database/migration/migrations/v6.0.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,41 +16,65 @@

-- Add Citrix RDP
INSERT INTO parameters (key, name, description, value, created_at, updated_at)
VALUES ('analysis.citrix_rdp_support', 'Citrix RDP Support', 'This configuration parameter toggles Citrix support during post-processing. When enabled, computers identified with a ''Direct Access Users'' local group will assume that Citrix is installed and CanRDP edges will require membership of both ''Direct Access Users'' and ''Remote Desktop Users'' local groups on the computer.', '{"enabled": false}',current_timestamp,current_timestamp) ON CONFLICT DO NOTHING;
VALUES ('analysis.citrix_rdp_support', 'Citrix RDP Support',
'This configuration parameter toggles Citrix support during post-processing. When enabled, computers identified with a ''Direct Access Users'' local group will assume that Citrix is installed and CanRDP edges will require membership of both ''Direct Access Users'' and ''Remote Desktop Users'' local groups on the computer.',
'{
"enabled": false
}', current_timestamp, current_timestamp)
ON CONFLICT DO NOTHING;

-- Add Prune TTLs
INSERT INTO parameters (key, name, description, value, created_at, updated_at) VALUES ('prune.ttl', 'Prune Retention TTL Configuration Parameters', 'This configuration parameter sets the retention TTLs during analysis pruning.', '{"base_ttl": "P7D", "has_session_edge_ttl": "P3D"}', current_timestamp, current_timestamp) ON CONFLICT DO NOTHING;
INSERT INTO parameters (key, name, description, value, created_at, updated_at)
VALUES ('prune.ttl', 'Prune Retention TTL Configuration Parameters',
'This configuration parameter sets the retention TTLs during analysis pruning.', '{
"base_ttl": "P7D",
"has_session_edge_ttl": "P3D"
}', current_timestamp, current_timestamp)
ON CONFLICT DO NOTHING;

-- Add Reconciliation to parameters and remove from feature_flags
INSERT INTO parameters (key, name, description, value, created_at, updated_at) VALUES ('analysis.reconciliation', 'Reconciliation', 'This configuration parameter enables / disables reconciliation during analysis.', format('{"enabled": %s}', (SELECT COALESCE((SELECT enabled FROM feature_flags WHERE key = 'reconciliation'), TRUE))::text)::json, current_timestamp, current_timestamp) ON CONFLICT DO NOTHING;
INSERT INTO parameters (key, name, description, value, created_at, updated_at)
VALUES ('analysis.reconciliation', 'Reconciliation',
'This configuration parameter enables / disables reconciliation during analysis.', format('{"enabled": %s}',
(SELECT COALESCE(
(SELECT enabled FROM feature_flags WHERE key = 'reconciliation'),
TRUE))::text)::json,
current_timestamp, current_timestamp)
ON CONFLICT DO NOTHING;
-- must occur after insert to ensure reconciliation flag is set to whatever current value is
DELETE FROM feature_flags WHERE key = 'reconciliation';
DELETE
FROM feature_flags
WHERE key = 'reconciliation';

-- Grant the Read-Only user SavedQueriesRead permissions
INSERT INTO roles_permissions (role_id, permission_id) VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'), (SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Read')) ON CONFLICT DO NOTHING;
INSERT INTO roles_permissions (role_id, permission_id)
VALUES ((SELECT id FROM roles WHERE roles.name = 'Read-Only'),
(SELECT id FROM permissions WHERE permissions.authority = 'saved_queries' and permissions.name = 'Read'))
ON CONFLICT DO NOTHING;

-- Add OIDC Support feature flag
INSERT INTO feature_flags (created_at, updated_at, key, name, description, enabled, user_updatable)
VALUES (
current_timestamp,
current_timestamp,
'oidc_support',
'OIDC Support',
'Enables OpenID Connect authentication support for SSO Authentication.',
false,
false
)
VALUES (current_timestamp,
current_timestamp,
'oidc_support',
'OIDC Support',
'Enables OpenID Connect authentication support for SSO Authentication.',
false,
false)
ON CONFLICT DO NOTHING;

do
$$
begin
-- Update existing Edge tables with an additional constraint to support ON CONFLICT upserts
alter table edge drop constraint if exists edge_graph_id_start_id_end_id_kind_id_key;
alter table edge add constraint edge_graph_id_start_id_end_id_kind_id_key unique (graph_id, start_id, end_id, kind_id);
alter table edge
drop constraint if exists edge_graph_id_start_id_end_id_kind_id_key;
alter table edge
add constraint edge_graph_id_start_id_end_id_kind_id_key unique (graph_id, start_id, end_id, kind_id);
exception
-- This guards against the possibility that the edge table doesn't exist, in which case there's no constraint to
-- migrate
when undefined_table then null;
end
$$;

13 changes: 13 additions & 0 deletions cmd/api/src/database/migration/migrations/v6.1.0.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-- OIDC Provider
CREATE TABLE IF NOT EXISTS oidc_providers
(
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
client_id TEXT NOT NULL,
issuer TEXT NOT NULL,

updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),

UNIQUE (name)
)
15 changes: 15 additions & 0 deletions cmd/api/src/database/mocks/db.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 39 additions & 0 deletions cmd/api/src/database/oidc.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

package database

import (
"context"

"github.com/specterops/bloodhound/src/model"
)

// OIDCProviderData defines the interface required to interact with the oidc_providers table
type OIDCProviderData interface {
CreateOIDCProvider(ctx context.Context, name, issuer, clientID string) (model.OIDCProvider, error)
}

// CreateOIDCProvider creates a new entry for an OIDC provider
func (s *BloodhoundDB) CreateOIDCProvider(ctx context.Context, name, issuer, clientID string) (model.OIDCProvider, error) {
provider := model.OIDCProvider{
Name: name,
ClientID: clientID,
Issuer: issuer,
}

return provider, CheckError(s.db.WithContext(ctx).Table("oidc_providers").Create(&provider))
}
Loading

0 comments on commit 34201d2

Please sign in to comment.