Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bed-4851 OIDC API Provider Registration #894

Merged
merged 23 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
97eb109
BED-4851 Updated CreateOIDCProvider endpoint and relevant database in…
mvlipka Sep 30, 2024
e7111dd
BED-4851 updates to conform to new design
mvlipka Oct 1, 2024
2a22dc6
BED-4851 cleanup import
mvlipka Oct 1, 2024
5028c18
BED-4851 added integration test for TestBloodhoundDB_CreateSSOProvide…
mvlipka Oct 2, 2024
686a359
BED-4851 removed name from oidc_providers
mvlipka Oct 2, 2024
6a67c61
BED-4851 updated tests with removed name field
mvlipka Oct 2, 2024
4c818e7
BED-4851 idempotent saml_providers constraint
mvlipka Oct 2, 2024
0723780
BED-4851 moved responsibility of slug creation to the db layer. Prope…
mvlipka Oct 3, 2024
022d2a6
Merge main
mvlipka Oct 3, 2024
6877860
generate
mvlipka Oct 3, 2024
704c330
removed test I was playing with and accidentally committed, oops. Add…
mvlipka Oct 3, 2024
e55f5e0
fix returning error from CreateSSOPRovider
mvlipka Oct 3, 2024
1cf416d
added test for invalid SessionAuthProvider
mvlipka Oct 3, 2024
0be0354
fixed test, better naming on sso provider types
mvlipka Oct 3, 2024
5b4edcc
removed no longer used SetupTransaction
mvlipka Oct 4, 2024
c98ff3e
fixed bug in listing audit logs. Added sso_provider_id to users table…
mvlipka Oct 7, 2024
2555790
fixed bug in listing audit logs
mvlipka Oct 7, 2024
76e0252
revert re-ordering auditlog time parameters
mvlipka Oct 7, 2024
f2ebadd
Merge branch 'main' into BED-4851-oidc-api-provider-registration-2
mvlipka Oct 7, 2024
1795e78
make sso_provider_id nullable on SAMLProvider and add sso_provider_id…
mvlipka Oct 7, 2024
c828f5f
backfill users with their appropriate sso_provider_id
mvlipka Oct 8, 2024
8157ef4
removed new constant for SSO Provider and revert back to AuthProvider
mvlipka Oct 8, 2024
87898f1
added client_id to audit logs and no longer delete users when their s…
mvlipka Oct 9, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cmd/api/src/api/registration/v2.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ func registerV2Auth(cfg config.Configuration, db database.Database, permissions
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),
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),
Expand Down
16 changes: 16 additions & 0 deletions cmd/api/src/api/tools/analysis_schedule.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// 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 tools

import (
Expand Down
16 changes: 16 additions & 0 deletions cmd/api/src/api/tools/analysis_schedule_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,19 @@
// 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 tools_test

import (
Expand Down
11 changes: 2 additions & 9 deletions cmd/api/src/api/v2/auth/oidc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package auth

import (
"net/http"
"strings"

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

Expand All @@ -42,17 +41,11 @@ func (s ManagementResource) CreateOIDCProvider(response http.ResponseWriter, req
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 {
if oidcProvider, err := s.db.CreateOIDCProvider(request.Context(), createRequest.Name, createRequest.Issuer, createRequest.ClientID); err != nil {
api.HandleDatabaseError(request, response, err)
} else {
api.WriteBasicResponse(request.Context(), provider, http.StatusCreated, response)
api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusCreated, response)
}
}
}
32 changes: 13 additions & 19 deletions cmd/api/src/api/v2/auth/oidc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,12 @@ package auth_test
import (
"fmt"
"net/http"

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

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

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

"testing"

"go.uber.org/mock/gomock"
)

Expand All @@ -43,19 +39,17 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
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: "",
mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "Bloodhound gang", "https://localhost/auth", "bloodhound").Return(model.OIDCProvider{
ClientID: "bloodhound",
Issuer: "https://localhost/auth",
}, nil)

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

Name: "Bloodhound gang",
Issuer: "https://localhost/auth",
ClientID: "bloodhound",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Expand All @@ -78,8 +72,9 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
Name: "test",
Issuer: "",
Name: "test",
Issuer: "1234:not:a:url",
ClientID: "bloodhound",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Require().
Expand All @@ -106,9 +101,8 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) {
WithMethod(http.MethodPost).
WithURL(url).
WithBody(auth.CreateOIDCProviderRequest{
Name: "test",
Issuer: "https://localhost/auth",

Name: "test",
Issuer: "https://localhost/auth",
ClientID: "bloodhound",
}).
OnHandlerFunc(resources.CreateOIDCProvider).
Expand Down
1 change: 1 addition & 0 deletions cmd/api/src/database/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ type Database interface {
DeleteSAMLProvider(ctx context.Context, samlProvider model.SAMLProvider) error

// SSO
SSOProviderData
OIDCProviderData

// Sessions
Expand Down
81 changes: 63 additions & 18 deletions cmd/api/src/database/migration/migrations/v6.1.0.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,74 @@
--
-- SPDX-License-Identifier: Apache-2.0

-- Add Scheduled Analysis Configs
INSERT INTO parameters (key, name, description, value, created_at, updated_at)
VALUES ('analysis.scheduled',
'Scheduled Analysis',
'This configuration parameter allows setting a schedule for analysis. When enabled, analysis will only run when the scheduled time arrives',
'{
"enabled": false,
"rrule": ""
}',
current_timestamp, current_timestamp)
ON CONFLICT DO NOTHING;

-- Add last analysis time to datapipe status so we can track scheduled analysis time properly
ALTER TABLE datapipe_status
ADD COLUMN IF NOT EXISTS "last_analysis_run_at" TIMESTAMP with time zone;

-- Create our sso_provider_type enum
DO
$$
BEGIN
CREATE TYPE sso_provider_type AS ENUM ('saml', 'oidc');
EXCEPTION
WHEN duplicate_object THEN null;
END
$$;

-- SSO Provider
CREATE TABLE IF NOT EXISTS sso_providers
(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL,
type sso_provider_type NOT NULL,

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

UNIQUE (name),
UNIQUE (slug)
);

-- 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,
id SERIAL PRIMARY KEY,
client_id TEXT NOT NULL,
issuer TEXT NOT NULL,
sso_provider_id INTEGER REFERENCES sso_providers (id) ON DELETE CASCADE NULL,
ddlees marked this conversation as resolved.
Show resolved Hide resolved

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

UNIQUE (name)
updated_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

-- Create the reference from saml_providers to sso_providers
ALTER TABLE ONLY saml_providers
ADD COLUMN IF NOT EXISTS sso_provider_id INTEGER NULL;
ALTER TABLE ONLY saml_providers
DROP CONSTRAINT IF EXISTS fk_saml_provider_sso_provider;
ALTER TABLE ONLY saml_providers
ADD CONSTRAINT fk_saml_provider_sso_provider FOREIGN KEY (sso_provider_id) REFERENCES sso_providers (id) ON DELETE CASCADE;

-- Add Scheduled Analysis Configs
INSERT INTO parameters (key, name, description, value, created_at, updated_at)
VALUES ('analysis.scheduled',
'Scheduled Analysis',
'This configuration parameter allows setting a schedule for analysis. When enabled, analysis will only run when the scheduled time arrives',
'{"enabled": false, "rrule": ""}',
current_timestamp,current_timestamp) ON CONFLICT DO NOTHING;
-- Backfill our sso_providers table with the existing data from saml_providers
INSERT INTO sso_providers(name, slug, type) (SELECT name, lower(replace(name, ' ', '-')), 'saml'
FROM saml_providers
WHERE sso_provider_id IS NULL)
ON CONFLICT DO NOTHING;

-- Add last analysis time to datapipe status so we can track scheduled analysis time properly
ALTER TABLE datapipe_status
ADD COLUMN IF NOT EXISTS "last_analysis_run_at" TIMESTAMP with time zone;
-- Backfill the references from the newly created sso_provider entries
UPDATE saml_providers
SET sso_provider_id = (SELECT id FROM sso_providers WHERE name = saml_providers.name)
WHERE saml_providers.sso_provider_id IS NULL;
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.

Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,37 @@ import (
"context"

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

const (
oidcProvidersTableName = "oidc_providers"
)

// 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
// CreateOIDCProvider creates a new entry for an OIDC provider as well as the associated SSO provider
func (s *BloodhoundDB) CreateOIDCProvider(ctx context.Context, name, issuer, clientID string) (model.OIDCProvider, error) {
provider := model.OIDCProvider{
Name: name,
oidcProvider := model.OIDCProvider{
ClientID: clientID,
Issuer: issuer,
}

return provider, CheckError(s.db.WithContext(ctx).Table("oidc_providers").Create(&provider))
// Create both the sso_providers and oidc_providers rows in a single transaction
// If one of these requests errors, both changes will be rolled back
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
bhdb := NewBloodhoundDB(tx, s.idResolver)

if ssoProvider, err := bhdb.CreateSSOProvider(ctx, name, model.SessionAuthProviderOIDC); err != nil {
return err
} else {
oidcProvider.SSOProviderID = int(ssoProvider.ID)
mistahj67 marked this conversation as resolved.
Show resolved Hide resolved
return CheckError(tx.WithContext(ctx).Table(oidcProvidersTableName).Create(&oidcProvider))
}
})

return oidcProvider, err
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ func TestBloodhoundDB_CreateOIDCProvider(t *testing.T) {
provider, err := dbInst.CreateOIDCProvider(testCtx, "test", "https://test.localhost.com/auth", "bloodhound")
require.NoError(t, err)

assert.Equal(t, "test", provider.Name)
assert.Equal(t, "https://test.localhost.com/auth", provider.Issuer)
assert.Equal(t, "bloodhound", provider.ClientID)
})
Expand Down
58 changes: 58 additions & 0 deletions cmd/api/src/database/sso_providers.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 database

import (
"context"
"fmt"
"strings"

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

const (
ssoProviderTableName = "sso_providers"
)

var (
mistahj67 marked this conversation as resolved.
Show resolved Hide resolved
ssoProviderTypeMapping = map[model.SessionAuthProvider]model.SSOProviderType{
model.SessionAuthProviderOIDC: model.SSOProviderTypeOIDC,
model.SessionAuthProviderSAML: model.SSOProviderTypeSAML,
}
)

// SSOProviderData defines the methods required to interact with the sso_providers table
type SSOProviderData interface {
CreateSSOProvider(ctx context.Context, name string, authType model.SessionAuthProvider) (model.SSOProvider, error)
}

// CreateSSOProvider creates an entry in the sso_providers table
// A slug will be created for the SSO Provider using the name argument as a base. The name will be lower cased and all spaces are replaced with `-`
func (s *BloodhoundDB) CreateSSOProvider(ctx context.Context, name string, authType model.SessionAuthProvider) (model.SSOProvider, error) {
if ssoProviderType, ok := ssoProviderTypeMapping[authType]; !ok {
return model.SSOProvider{}, fmt.Errorf("error could not find a valid mapping from SessionAuthProvider to SSOProviderType: %d", authType)
} else {

provider := model.SSOProvider{
Name: name,
Slug: strings.ToLower(strings.ReplaceAll(name, " ", "-")),
Type: ssoProviderType,
}

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