From 348c669ad254bd31bacaec70ea0fd96c395b4856 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Mon, 23 Dec 2024 14:24:13 -0800 Subject: [PATCH 1/9] BED-5072 fix local dev infinite loop (#1008) --- cmd/ui/src/main.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ui/src/main.tsx b/cmd/ui/src/main.tsx index 00f27bdb05..5f770d15e7 100644 --- a/cmd/ui/src/main.tsx +++ b/cmd/ui/src/main.tsx @@ -84,7 +84,7 @@ const main = async () => { const rootContainer = document.getElementById('root'); const root = createRoot(rootContainer!); - if (import.meta.env.DEV) { + if (import.meta.env.DEV && location.pathname.startsWith('/ui/')) { const { worker } = await import('./mocks/browser'); await worker.start({ serviceWorker: { From d4d10bf30db7e470a106a49148b94ab55bd33d7b Mon Sep 17 00:00:00 2001 From: mistahj67 <26472282+mistahj67@users.noreply.github.com> Date: Tue, 24 Dec 2024 10:53:05 -1000 Subject: [PATCH 2/9] BED-5178 fix: OIDC default scopes (#1041) --- cmd/api/src/api/v2/auth/oidc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/api/src/api/v2/auth/oidc.go b/cmd/api/src/api/v2/auth/oidc.go index 4804862082..a76b1da00c 100644 --- a/cmd/api/src/api/v2/auth/oidc.go +++ b/cmd/api/src/api/v2/auth/oidc.go @@ -124,7 +124,7 @@ func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, reque ClientID: ssoProvider.OIDCProvider.ClientID, Endpoint: provider.Endpoint(), RedirectURL: getRedirectURL(request, ssoProvider), - Scopes: []string{"openid", "profile", "email", "email_verified", "name", "given_name", "family_name"}, + Scopes: []string{"openid", "profile", "email"}, } // use PKCE to protect against CSRF attacks From cd4233ad1ad198aa76fd814251161bae407cedce Mon Sep 17 00:00:00 2001 From: Francisco Quintero Date: Thu, 26 Dec 2024 10:20:16 -0500 Subject: [PATCH 3/9] chore: updated doodle ui to to 1.0.0-alpha.13 signed (#1040) --- ...pm-1.0.0-alpha.12-81cbc222a8-df3f49e535.zip | Bin 154624 -> 0 bytes ...pm-1.0.0-alpha.13-c77d910f01-17ceaffee9.zip | Bin 0 -> 155616 bytes cmd/ui/package.json | 2 +- packages/javascript/bh-shared-ui/package.json | 2 +- yarn.lock | 12 ++++++------ 5 files changed, 8 insertions(+), 8 deletions(-) delete mode 100644 .yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.12-81cbc222a8-df3f49e535.zip create mode 100644 .yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.13-c77d910f01-17ceaffee9.zip diff --git a/.yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.12-81cbc222a8-df3f49e535.zip b/.yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.12-81cbc222a8-df3f49e535.zip deleted file mode 100644 index b8f123e0c57a290b35faf2f6bc5a14c23bea39bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 154624 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 J`tXm@e*wb`{SW{E diff --git a/.yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.13-c77d910f01-17ceaffee9.zip b/.yarn/cache/@bloodhoundenterprise-doodleui-npm-1.0.0-alpha.13-c77d910f01-17ceaffee9.zip new file mode 100644 index 0000000000000000000000000000000000000000..15712fc5b14b34a45953b002a95f0e95abba49ac GIT binary patch literal 155616 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 literal 0 HcmV?d00001 diff --git a/cmd/ui/package.json b/cmd/ui/package.json index 5115ad5e34..e4aa44aec0 100644 --- a/cmd/ui/package.json +++ b/cmd/ui/package.json @@ -16,7 +16,7 @@ "check-format": "prettier --list-different \"src/**/*.@(js|jsx|ts|tsx|md|html|css|scss|json)\"" }, "dependencies": { - "@bloodhoundenterprise/doodleui": "^1.0.0-alpha.12", + "@bloodhoundenterprise/doodleui": "^1.0.0-alpha.13", "@date-io/luxon": "^1.3.13", "@emotion/react": "^11.10.4", "@emotion/styled": "^11.10.4", diff --git a/packages/javascript/bh-shared-ui/package.json b/packages/javascript/bh-shared-ui/package.json index 7e19a18398..180249e5fc 100644 --- a/packages/javascript/bh-shared-ui/package.json +++ b/packages/javascript/bh-shared-ui/package.json @@ -19,7 +19,7 @@ "author": "The BloodHound Enterprise Team (https://bloodhoundenterprise.io/)", "license": "MIT", "dependencies": { - "@bloodhoundenterprise/doodleui": "^1.0.0-alpha.12", + "@bloodhoundenterprise/doodleui": "^1.0.0-alpha.13", "@fortawesome/fontawesome-free": "^6.4.2", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-solid-svg-icons": "^6.4.2", diff --git a/yarn.lock b/yarn.lock index 67fe334379..e1820dffb9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -309,9 +309,9 @@ __metadata: languageName: node linkType: hard -"@bloodhoundenterprise/doodleui@npm:^1.0.0-alpha.12": - version: 1.0.0-alpha.12 - resolution: "@bloodhoundenterprise/doodleui@npm:1.0.0-alpha.12" +"@bloodhoundenterprise/doodleui@npm:^1.0.0-alpha.13": + version: 1.0.0-alpha.13 + resolution: "@bloodhoundenterprise/doodleui@npm:1.0.0-alpha.13" dependencies: "@radix-ui/react-accordion": ^1.1.2 "@radix-ui/react-checkbox": ^1.1.2 @@ -338,7 +338,7 @@ __metadata: react: ^18.2.0 react-dom: ^18.2.0 tailwindcss: ^3.4.3 - checksum: df3f49e535094fdb35349d5e8332a5ac5692ec3bac7fe31f4dd807f69ea21a48bb9d8f9a4e0f82a864135442214295e2d657c0fee54558b5535d98193d9e70ba + checksum: 17ceaffee9101e5e1fb84dca57893758040f6adc543c35d956ac6bb1590b6dc665086c259a23d3a995eea4a99539a9fdf1c6fa11401ee79375b71525ecbb6ba7 languageName: node linkType: hard @@ -4235,7 +4235,7 @@ __metadata: version: 0.0.0-use.local resolution: "bh-shared-ui@workspace:packages/javascript/bh-shared-ui" dependencies: - "@bloodhoundenterprise/doodleui": ^1.0.0-alpha.12 + "@bloodhoundenterprise/doodleui": ^1.0.0-alpha.13 "@emotion/react": ^11.10.4 "@emotion/styled": ^11.10.4 "@fortawesome/fontawesome-free": ^6.4.2 @@ -4343,7 +4343,7 @@ __metadata: version: 0.0.0-use.local resolution: "bloodhound-ui@workspace:cmd/ui" dependencies: - "@bloodhoundenterprise/doodleui": ^1.0.0-alpha.12 + "@bloodhoundenterprise/doodleui": ^1.0.0-alpha.13 "@date-io/luxon": ^1.3.13 "@emotion/react": ^11.10.4 "@emotion/styled": ^11.10.4 From c7047753446467f9b8e45d5c4def3efffcf7678b Mon Sep 17 00:00:00 2001 From: Eli K Miller Date: Mon, 30 Dec 2024 07:47:34 -0800 Subject: [PATCH 4/9] Fix typescript errors in `bh-shared-ui` (#1042) * chore: ensure type check runs before build in `bh-shared-ui` and `js-client-library` * feat: added new github workflow `build-ui.yml` * chore: fix typescript errors in `bh-shared-ui` * chore: add `js-file-download` to list of external dependencies when building `bh-shared-ui` * chore: rename job, remove "BED-5219" from list of branches that the job runs on when a new commit is pushed --- .github/workflows/build-ui.yml | 48 +++++++++++++++++++ packages/javascript/bh-shared-ui/package.json | 2 +- .../javascript/bh-shared-ui/rollup.config.js | 1 + .../CreateUserDialog.test.tsx | 8 ++++ .../LoginViaSSOForm/LoginViaSSOForm.test.tsx | 4 ++ .../SSOProviderInfoPanel.test.tsx | 4 ++ .../SSOProviderTable.test.tsx | 7 +++ .../UpdateUserDialog.test.tsx | 8 ++++ .../UpsertSAMLProviderForm.test.tsx | 8 ++-- .../SSOConfiguration.test.tsx | 4 ++ .../src/views/Users/Users.test.tsx | 2 + .../javascript/js-client-library/package.json | 2 +- 12 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/build-ui.yml diff --git a/.github/workflows/build-ui.yml b/.github/workflows/build-ui.yml new file mode 100644 index 0000000000..48c5c1f2ae --- /dev/null +++ b/.github/workflows/build-ui.yml @@ -0,0 +1,48 @@ +# 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 + +name: Build UI + +on: + push: + branches: [main] + pull_request: + types: [opened, synchronize] + +jobs: + build-ui: + runs-on: ubuntu-latest + + steps: + - name: Checkout source code for this repository + uses: actions/checkout@v3 + + - name: Install Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install Yarn + run: | + npm install --global yarn + + - name: Install Deps + run: | + cd cmd/ui && yarn + + - name: Run Build + run: | + cd cmd/ui && yarn build diff --git a/packages/javascript/bh-shared-ui/package.json b/packages/javascript/bh-shared-ui/package.json index 180249e5fc..2d00a3e896 100644 --- a/packages/javascript/bh-shared-ui/package.json +++ b/packages/javascript/bh-shared-ui/package.json @@ -9,7 +9,7 @@ ], "type": "module", "scripts": { - "build": "rollup -c rollup.config.js", + "build": "yarn check-types && rollup -c rollup.config.js", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "test": "TZ=America/Los_Angeles vitest", "check-types": "tsc --noEmit --pretty", diff --git a/packages/javascript/bh-shared-ui/rollup.config.js b/packages/javascript/bh-shared-ui/rollup.config.js index b976f9ded5..c238461c10 100644 --- a/packages/javascript/bh-shared-ui/rollup.config.js +++ b/packages/javascript/bh-shared-ui/rollup.config.js @@ -57,6 +57,7 @@ export default { 'notistack', 'react-query', 'js-client-library', + 'js-file-download', 'swagger-ui-react', 'swagger-ui-react/swagger-ui.css', 'prop-types', diff --git a/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx b/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx index 088a3c3cbf..f13e0ea360 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx @@ -43,6 +43,8 @@ const testSSOProviders: SSOProvider[] = [ sp_metadata_uri: '', sp_acs_uri: '', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '', updated_at: '', }, @@ -60,6 +62,8 @@ const testSSOProviders: SSOProvider[] = [ sp_metadata_uri: '', sp_acs_uri: '', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '', updated_at: '', }, @@ -77,6 +81,8 @@ const testSSOProviders: SSOProvider[] = [ sp_metadata_uri: '', sp_acs_uri: '', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '', updated_at: '', }, @@ -94,6 +100,8 @@ const testSSOProviders: SSOProvider[] = [ sp_metadata_uri: '', sp_acs_uri: '', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '', updated_at: '', }, diff --git a/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx b/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx index 42be06a211..292742ccbf 100644 --- a/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx @@ -32,6 +32,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '', updated_at: '', }, + login_uri: '', + callback_uri: '', id: 1, created_at: '', updated_at: '', @@ -55,6 +57,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '', updated_at: '', }, + login_uri: '', + callback_uri: '', id: 2, created_at: '', updated_at: '', diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx index 1763464ccc..0f09c29609 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx @@ -33,6 +33,8 @@ const samlProvider: SSOProvider = { sp_metadata_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-2/metadata', sp_acs_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-2/acs', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', }; @@ -46,6 +48,8 @@ const oidcProvider: SSOProvider = { issuer: 'http://bloodhound.localhost/test-idp-2', client_id: 'gotham-oidc', } as OIDCProviderInfo, + login_uri: '', + callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', }; diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx index 220df2ca0f..6997e7c343 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx @@ -25,6 +25,8 @@ const samlProvider: SSOProvider = { slug: 'gotham-saml', name: 'Gotham SAML', type: 'SAML', + login_uri: '', + callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', details: {} as SAMLProviderInfo, @@ -35,6 +37,8 @@ const oidcProvider: SSOProvider = { slug: 'gotham-oidc', name: 'Gotham OIDC', type: 'OIDC', + login_uri: '', + callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', details: {} as OIDCProviderInfo, @@ -45,6 +49,7 @@ const ssoProviders = [samlProvider, oidcProvider]; describe('SSOProviderTable', () => { const onClickSSOProvider = vi.fn(); const onDeleteSSOProvider = vi.fn(); + const onUpdateSSOProvider = vi.fn(); it('should render', async () => { const onToggleTypeSortOrder = vi.fn(); @@ -55,6 +60,7 @@ describe('SSOProviderTable', () => { loading={false} onClickSSOProvider={onClickSSOProvider} onDeleteSSOProvider={onDeleteSSOProvider} + onUpdateSSOProvider={onUpdateSSOProvider} onToggleTypeSortOrder={onToggleTypeSortOrder} /> ); @@ -82,6 +88,7 @@ describe('SSOProviderTable', () => { loading={false} onClickSSOProvider={onClickSSOProvider} onDeleteSSOProvider={onDeleteSSOProvider} + onUpdateSSOProvider={onUpdateSSOProvider} onToggleTypeSortOrder={onToggleTypeSortOrder} typeSortOrder={typeSortOrder} /> diff --git a/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx b/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx index 37a26c9290..dd7a03e515 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx @@ -48,6 +48,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', }, + login_uri: '', + callback_uri: '', id: 1, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', @@ -71,6 +73,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', }, + login_uri: '', + callback_uri: '', id: 2, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', @@ -94,6 +98,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', }, + login_uri: '', + callback_uri: '', id: 3, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', @@ -117,6 +123,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', }, + login_uri: '', + callback_uri: '', id: 4, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx index 518af2ec47..31c6f8cc4b 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx @@ -22,7 +22,7 @@ describe('UpsertSAMLProviderForm', () => { it('should render inputs, labels, and action buttons', () => { const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); expect(screen.getByLabelText('SAML Provider Name')).toBeInTheDocument(); @@ -37,7 +37,7 @@ describe('UpsertSAMLProviderForm', () => { const user = userEvent.setup(); const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); await user.click(screen.getByRole('button', { name: 'Cancel' })); @@ -48,7 +48,7 @@ describe('UpsertSAMLProviderForm', () => { const user = userEvent.setup(); const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); await user.click(screen.getByRole('button', { name: 'Submit' })); @@ -65,7 +65,7 @@ describe('UpsertSAMLProviderForm', () => { const testOnSubmit = vi.fn(); const validProviderName = 'test-provider-name'; const validMetadata = new File([], 'test-metadata.xml'); - render(); + render(); await user.type(screen.getByLabelText('SAML Provider Name'), validProviderName); diff --git a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx index c20baa6a91..d6a13ebbbf 100644 --- a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx @@ -35,6 +35,8 @@ const initialSAMLProvider: SSOProvider = { sp_metadata_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-1/metadata', sp_acs_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-1/acs', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', }; @@ -55,6 +57,8 @@ const newSAMLProvider: SSOProvider = { sp_metadata_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-2/metadata', sp_acs_uri: 'http://bloodhound.localhost/api/v2/login/saml/test-idp-2/acs', } as SAMLProviderInfo, + login_uri: '', + callback_uri: '', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; diff --git a/packages/javascript/bh-shared-ui/src/views/Users/Users.test.tsx b/packages/javascript/bh-shared-ui/src/views/Users/Users.test.tsx index bab27dc271..d116dc1523 100644 --- a/packages/javascript/bh-shared-ui/src/views/Users/Users.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/Users/Users.test.tsx @@ -113,6 +113,8 @@ const testSSOProviders: SSOProvider[] = [ created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', }, + login_uri: '', + callback_uri: '', id: 1, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', diff --git a/packages/javascript/js-client-library/package.json b/packages/javascript/js-client-library/package.json index 3a1a8d0fe0..571cff39ac 100644 --- a/packages/javascript/js-client-library/package.json +++ b/packages/javascript/js-client-library/package.json @@ -9,7 +9,7 @@ "README.md" ], "scripts": { - "build": "rollup --config rollup.config.js", + "build": "yarn check-types && rollup --config rollup.config.js", "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "test": "echo \"Warning: no test specified\"", "check-types": "tsc --noEmit --pretty", From 3c8334597806ab4742299cdc6cc3898513c9edea Mon Sep 17 00:00:00 2001 From: Alyx Holms Date: Thu, 2 Jan 2025 12:38:50 -0700 Subject: [PATCH 5/9] BED-4963: Clean Up and Tighten Enforcement of Linters (#1037) * chore: refactor out bh errors package * chore: clean up remaining linter warnings * chore: no longer warnings, only errors * chore: enable true pain (errcheck warnings) * chore: clean up from PR nits --- .golangci.json | 32 ++----- cmd/api/src/api/agi.go | 4 +- cmd/api/src/api/agi_test.go | 4 +- cmd/api/src/api/auth.go | 20 ++-- cmd/api/src/api/marshalling.go | 20 ++-- cmd/api/src/api/signature.go | 10 +- cmd/api/src/api/signature_internal_test.go | 2 +- cmd/api/src/api/tools/analysis_schedule.go | 12 +-- cmd/api/src/api/tools/trace.go | 8 +- cmd/api/src/api/v2/ad_entity_test.go | 2 +- cmd/api/src/api/v2/ad_related_entity_test.go | 2 +- cmd/api/src/api/v2/agi_test.go | 2 +- cmd/api/src/api/v2/apiclient/apiclient.go | 2 +- cmd/api/src/api/v2/app_config_test.go | 9 +- cmd/api/src/api/v2/attack_path.go | 2 +- cmd/api/src/api/v2/auth/auth.go | 24 ++--- cmd/api/src/api/v2/auth/auth_test.go | 16 ++-- cmd/api/src/api/v2/auth/login.go | 4 +- .../src/api/v2/auth/login_internal_test.go | 2 +- cmd/api/src/api/v2/auth/saml_internal_test.go | 4 +- cmd/api/src/api/v2/auth_integration_test.go | 2 +- cmd/api/src/api/v2/azure.go | 14 +-- cmd/api/src/api/v2/cypherquery.go | 6 +- cmd/api/src/api/v2/dataquality.go | 14 +-- cmd/api/src/api/v2/dataquality_test.go | 2 +- cmd/api/src/api/v2/file_uploads_test.go | 2 +- cmd/api/src/api/v2/flag_test.go | 7 +- cmd/api/src/api/v2/helpers.go | 4 +- cmd/api/src/api/v2/pathfinding_test.go | 2 +- cmd/api/src/api/v2/search_test.go | 7 +- cmd/api/src/auth/model.go | 2 +- cmd/api/src/auth/totp.go | 4 +- cmd/api/src/auth/totp_test.go | 2 +- cmd/api/src/daemons/api/bhapi/api.go | 2 +- cmd/api/src/daemons/api/toolapi/api.go | 2 +- .../src/daemons/datapipe/azure_convertors.go | 4 +- cmd/api/src/daemons/datapipe/jobs.go | 12 +-- cmd/api/src/database/audit.go | 6 +- cmd/api/src/database/auth.go | 7 +- cmd/api/src/database/db.go | 15 ++- cmd/api/src/database/log.go | 3 +- cmd/api/src/database/types/null/int32_test.go | 3 +- cmd/api/src/database/types/object.go | 4 +- cmd/api/src/model/filter.go | 6 +- cmd/api/src/model/ingest/ingest.go | 2 +- cmd/api/src/model/saved_queries.go | 2 +- cmd/api/src/model/search.go | 20 ++-- cmd/api/src/model/search_test.go | 8 +- cmd/api/src/model/sso_provider.go | 2 - cmd/api/src/queries/graph.go | 4 +- cmd/api/src/queries/graph_internal_test.go | 2 +- cmd/api/src/utils/util.go | 12 ++- .../utils/validation/duration_validator.go | 4 - cmd/api/src/version/version.go | 20 ++-- go.work | 1 - packages/go/analysis/azure/azure.go | 9 +- packages/go/analysis/azure/post.go | 36 ++----- packages/go/crypto/argon2.go | 10 +- packages/go/cypher/models/cypher/copy.go | 3 + .../cypher/models/pgsql/translate/delete.go | 4 +- .../go/cypher/models/pgsql/translate/model.go | 13 --- .../models/pgsql/translate/translator.go | 14 --- .../cypher/models/pgsql/translate/update.go | 4 +- packages/go/dawgs/drivers/pg/transaction.go | 2 +- packages/go/dawgs/graph/graph.go | 4 +- packages/go/dawgs/ops/parallel_test.go | 2 +- packages/go/dawgs/ops/traversal.go | 2 +- packages/go/dawgs/traversal/id.go | 2 +- packages/go/dawgs/traversal/traversal.go | 2 +- packages/go/ein/azure.go | 12 +-- packages/go/errors/error.go | 95 ------------------- packages/go/errors/error_test.go | 56 ----------- packages/go/errors/go.mod | 30 ------ packages/go/errors/go.sum | 21 ---- 74 files changed, 218 insertions(+), 493 deletions(-) delete mode 100644 packages/go/errors/error.go delete mode 100644 packages/go/errors/error_test.go delete mode 100644 packages/go/errors/go.mod delete mode 100644 packages/go/errors/go.sum diff --git a/.golangci.json b/.golangci.json index 5554b2549a..7616224cab 100644 --- a/.golangci.json +++ b/.golangci.json @@ -1,8 +1,6 @@ { "linters": { - "disable": [ - "errcheck" - ], + "disable": [], "enable": [ "gosimple", "stylecheck" @@ -14,17 +12,19 @@ "exclude-rules": [ { "path": ".go", - "text": "((neo4j(.+)(NewDriver|Result))|Id|database.Database|(.+)Deprecated) is deprecated" + "text": "((neo4j(.+)(NewDriver|Result))|Id|database.Database|(.+)Deprecated|batch.CreateRelationshipByIDs|jwt.StandardClaims) is deprecated" + }, + { + "path": "hyperloglog_bench_test.go", + "text": "SA6002:" }, { "path": "cache_test\\.go", - "text": "SA1026:", - "severity": "warning" + "text": "SA1026:" }, { "path": "foldr_test\\.go", - "text": "SA4000:", - "severity": "warning" + "text": "SA4000:" }, { "path": "dawgs/util/size/(.+)", @@ -45,21 +45,7 @@ "default-severity": "error", "rules": [ { - "linters": ["stylecheck", "gosimple", "unused", "errcheck", "forcetypeassert"], - "severity": "warning" - }, - { - "text": "SA1019:", - "severity": "warning" - }, - { - "path": "hyperloglog_bench_test\\.go", - "text": "SA6002:", - "severity": "warning" - }, - { - "path": "expected_ingest.go", - "text": "ST1022:", + "linters": ["errcheck"], "severity": "warning" } ] diff --git a/cmd/api/src/api/agi.go b/cmd/api/src/api/agi.go index 9923ad081d..d420a754a5 100644 --- a/cmd/api/src/api/agi.go +++ b/cmd/api/src/api/agi.go @@ -95,11 +95,11 @@ func (s AssetGroupMembers) Filter(filterMap model.QueryParameterFilterMap) (Asse result := s for column, filters := range filterMap { if validPredicates, err := s.GetValidFilterPredicatesAsStrings(column); err != nil { - return AssetGroupMembers{}, fmt.Errorf("%s: %s", model.ErrorResponseDetailsColumnNotFilterable, column) + return AssetGroupMembers{}, fmt.Errorf("%s: %s", model.ErrResponseDetailsColumnNotFilterable, column) } else { for _, filter := range filters { if !slices.Contains(validPredicates, string(filter.Operator)) { - return AssetGroupMembers{}, fmt.Errorf("%s: %s, %s", model.ErrorResponseDetailsFilterPredicateNotSupported, column, string(filter.Operator)) + return AssetGroupMembers{}, fmt.Errorf("%s: %s, %s", model.ErrResponseDetailsFilterPredicateNotSupported, column, string(filter.Operator)) } else if conditional, err := s.BuildFilteringConditional(column, filter.Operator, filter.Value); err != nil { return AssetGroupMembers{}, err } else { diff --git a/cmd/api/src/api/agi_test.go b/cmd/api/src/api/agi_test.go index 257fde4bf5..4dc7b88594 100644 --- a/cmd/api/src/api/agi_test.go +++ b/cmd/api/src/api/agi_test.go @@ -132,7 +132,7 @@ func TestAssetGroupMembers_Filter_Equals(t *testing.T) { }, }) require.NotNil(t, err) - require.Contains(t, err.Error(), model.ErrorResponseDetailsColumnNotFilterable) + require.Contains(t, err.Error(), model.ErrResponseDetailsColumnNotFilterable) _, err = input.Filter(model.QueryParameterFilterMap{ "object_id": model.QueryParameterFilters{ @@ -144,7 +144,7 @@ func TestAssetGroupMembers_Filter_Equals(t *testing.T) { }, }) require.NotNil(t, err) - require.Contains(t, err.Error(), model.ErrorResponseDetailsFilterPredicateNotSupported) + require.Contains(t, err.Error(), model.ErrResponseDetailsFilterPredicateNotSupported) // filter on object_id output, err := input.Filter(model.QueryParameterFilterMap{ diff --git a/cmd/api/src/api/auth.go b/cmd/api/src/api/auth.go index 0c95aebec2..559ccba049 100644 --- a/cmd/api/src/api/auth.go +++ b/cmd/api/src/api/auth.go @@ -24,6 +24,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/base64" + "errors" "fmt" "io" "net/http" @@ -34,7 +35,6 @@ import ( "github.com/gofrs/uuid" "github.com/golang-jwt/jwt/v4" "github.com/specterops/bloodhound/crypto" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/auth" @@ -45,12 +45,12 @@ import ( "github.com/specterops/bloodhound/src/model" ) -const ( - ErrInvalidAuth = errors.Error("invalid authentication") - ErrNoUserSecret = errors.Error("user does not have a secret auth provider registered") - ErrUserDisabled = errors.Error("user disabled") - ErrorUserNotAuthorizedForProvider = errors.Error("user not authorized for this provider") - ErrorInvalidAuthProvider = errors.Error("invalid auth provider") +var ( + ErrInvalidAuth = errors.New("invalid authentication") + ErrNoUserSecret = errors.New("user does not have a secret auth provider registered") + ErrUserDisabled = errors.New("user disabled") + ErrUserNotAuthorizedForProvider = errors.New("user not authorized for this provider") + ErrInvalidAuthProvider = errors.New("invalid auth provider") ) func parseRequestDate(rawDate string) (time.Time, error) { @@ -239,7 +239,7 @@ func (s authenticator) ValidateRequestSignature(tokenID uuid.UUID, request *http } else if authContext, err := s.ctxInitializer.InitContextFromToken(request.Context(), authToken); err != nil { return handleAuthDBError(err) } else if user, isUser := auth.GetUserFromAuthCtx(authContext); isUser && user.IsDisabled { - return authContext, http.StatusForbidden, errors.Error("user disabled") + return authContext, http.StatusForbidden, ErrUserDisabled } else if err := validateRequestTime(serverTime, requestDate); err != nil { return auth.Context{}, http.StatusUnauthorized, err } else { @@ -384,7 +384,7 @@ func (s authenticator) CreateSSOSession(request *http.Request, response http.Res } } else { if !user.SSOProviderID.Valid || ssoProvider.ID != user.SSOProviderID.Int32 { - auditLogFields["error"] = ErrorUserNotAuthorizedForProvider + auditLogFields["error"] = ErrUserNotAuthorizedForProvider WriteErrorResponse(requestCtx, BuildErrorResponse(http.StatusForbidden, "user is not allowed", request), response) return } @@ -436,7 +436,7 @@ func (s authenticator) CreateSession(ctx context.Context, user model.User, authP userSession.AuthProviderType = model.SessionAuthProviderOIDC userSession.AuthProviderID = typedAuthProvider.ID default: - return "", ErrorInvalidAuthProvider + return "", ErrInvalidAuthProvider } if newSession, err := s.db.CreateUserSession(ctx, userSession); err != nil { diff --git a/cmd/api/src/api/marshalling.go b/cmd/api/src/api/marshalling.go index edf734d117..1958d889d3 100644 --- a/cmd/api/src/api/marshalling.go +++ b/cmd/api/src/api/marshalling.go @@ -19,12 +19,12 @@ package api import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" "time" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/mediatypes" @@ -36,9 +36,11 @@ import ( const ( // DefaultAPIPayloadReadLimitBytes sets the maximum API body size to 10MB DefaultAPIPayloadReadLimitBytes = 10 * 1024 * 1024 +) - ErrorContentTypeJson = errors.Error("content type must be application/json") - ErrorNoRequestBody = errors.Error("request body is empty") +var ( + ErrContentTypeJson = errors.New("content type must be application/json") + ErrNoRequestBody = errors.New("request body is empty") ) // These are the standardized API V2 response structures @@ -190,7 +192,7 @@ func WriteBinaryResponse(_ context.Context, data []byte, filename string, status func ReadJsonResponsePayload(value any, response *http.Response) error { if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) { - return ErrorContentTypeJson + return ErrContentTypeJson } decoder := json.NewDecoder(response.Body) @@ -203,7 +205,7 @@ func ReadJsonResponsePayload(value any, response *http.Response) error { func ReadAPIV2ResponsePayload(value any, response *http.Response) error { if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) { - return ErrorContentTypeJson + return ErrContentTypeJson } var wrapper BasicResponse @@ -221,7 +223,7 @@ func ReadAPIV2ResponsePayload(value any, response *http.Response) error { func ReadAPIV2ResponseWrapperPayload(value any, response *http.Response) error { if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) { - return ErrorContentTypeJson + return ErrContentTypeJson } if content, err := io.ReadAll(response.Body); err != nil { @@ -235,7 +237,7 @@ func ReadAPIV2ResponseWrapperPayload(value any, response *http.Response) error { func ReadAPIV2ErrorResponsePayload(value *ErrorWrapper, response *http.Response) error { if !utils.HeaderMatches(response.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) { - return ErrorContentTypeJson + return ErrContentTypeJson } if content, err := io.ReadAll(response.Body); err != nil { @@ -249,11 +251,11 @@ func ReadAPIV2ErrorResponsePayload(value *ErrorWrapper, response *http.Response) func ReadJSONRequestPayloadLimited(value any, request *http.Request) error { if !utils.HeaderMatches(request.Header, headers.ContentType.String(), mediatypes.ApplicationJson.String()) { - return ErrorContentTypeJson + return ErrContentTypeJson } if request.Body == nil { - return ErrorNoRequestBody + return ErrNoRequestBody } var ( diff --git a/cmd/api/src/api/signature.go b/cmd/api/src/api/signature.go index 9ecf967059..2d7c29d15e 100644 --- a/cmd/api/src/api/signature.go +++ b/cmd/api/src/api/signature.go @@ -32,7 +32,7 @@ import ( "github.com/specterops/bloodhound/headers" ) -const ErrorTemplateHMACSignature string = "unable to compute hmac signature: %w" +const ErrTemplateHMACSignature string = "unable to compute hmac signature: %w" // tee takes a source reader and two writers. The function reads from the source until exhaustion. Each read is written // serially to both writers. @@ -138,7 +138,7 @@ func (s *SelfDestructingTempFile) Name() string { // NOTE: The given io.Reader will be read to EOF. Consider using io.TeeReader so that the body may be read again after the signature has been created. func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key string, datetime string, requestMethod string, requestURI string, body io.Reader) ([]byte, error) { if hasher == nil { - return nil, fmt.Errorf(ErrorTemplateHMACSignature, fmt.Errorf("hasher must not be nil")) + return nil, fmt.Errorf(ErrTemplateHMACSignature, fmt.Errorf("hasher must not be nil")) } digester := hmac.New(hasher, []byte(key)) @@ -150,7 +150,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin // Example: GET /api/v2/test/resource HTTP/1.1 // Signature Component: GET/api/v2/test/resource if _, err := digester.Write([]byte(requestMethod + requestURI)); err != nil { - return nil, fmt.Errorf(ErrorTemplateHMACSignature, err) + return nil, fmt.Errorf(ErrTemplateHMACSignature, err) } // DateKey is the next HMAC digest link in the signature chain. This encodes the RFC3339 formatted datetime @@ -163,7 +163,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin digester = hmac.New(hasher, digester.Sum(nil)) if _, err := digester.Write([]byte(datetime[:13])); err != nil { - return nil, fmt.Errorf(ErrorTemplateHMACSignature, err) + return nil, fmt.Errorf(ErrTemplateHMACSignature, err) } // Body signing is the last HMAC digest link in the signature chain. This encodes the request body as part of @@ -179,7 +179,7 @@ func NewRequestSignature(ctx context.Context, hasher func() hash.Hash, key strin if body != nil { if _, err := io.Copy(digester, body); err != nil { - return nil, fmt.Errorf(ErrorTemplateHMACSignature, err) + return nil, fmt.Errorf(ErrTemplateHMACSignature, err) } } diff --git a/cmd/api/src/api/signature_internal_test.go b/cmd/api/src/api/signature_internal_test.go index 9adf54e03d..057bd576e1 100644 --- a/cmd/api/src/api/signature_internal_test.go +++ b/cmd/api/src/api/signature_internal_test.go @@ -20,13 +20,13 @@ import ( "bytes" "context" "crypto/sha256" + "errors" "io" "strings" "testing" "testing/iotest" "time" - "github.com/specterops/bloodhound/errors" "github.com/stretchr/testify/require" ) diff --git a/cmd/api/src/api/tools/analysis_schedule.go b/cmd/api/src/api/tools/analysis_schedule.go index df2afe5053..4163afeaa5 100644 --- a/cmd/api/src/api/tools/analysis_schedule.go +++ b/cmd/api/src/api/tools/analysis_schedule.go @@ -33,12 +33,12 @@ type ScheduledAnalysisConfiguration struct { RRule string `json:"rrule"` } -const ErrorInvalidRrule = "invalid rrule specified: %v" -const ErrorFailedRetrievingData = "error retrieving configuration data: %v" +const ErrInvalidRrule = "invalid rrule specified: %v" +const ErrFailedRetrievingData = "error retrieving configuration data: %v" func (s ToolContainer) GetScheduledAnalysisConfiguration(response http.ResponseWriter, request *http.Request) { if config, err := appcfg.GetScheduledAnalysisParameter(request.Context(), s.db); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf(ErrorFailedRetrievingData, err), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, fmt.Sprintf(ErrFailedRetrievingData, err), request), response) } else { api.WriteJSONResponse(request.Context(), config, http.StatusOK, response) } @@ -70,11 +70,11 @@ func (s ToolContainer) SetScheduledAnalysisConfiguration(response http.ResponseW //Validate that the rrule is a good rule. We're going to require a DTSTART to keep scheduling consistent. //We're also going to reject UNTIL/COUNT because it will most likely break the pipeline once it's hit without being invalid if _, err := rrule.StrToRRule(config.RRule); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, err), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, err), request), response) } else if strings.Contains(strings.ToUpper(config.RRule), "UNTIL") || strings.Contains(strings.ToUpper(config.RRule), "COUNT") { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, "count/until not supported"), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, "count/until not supported"), request), response) } else if !strings.Contains(strings.ToUpper(config.RRule), "DTSTART") { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRrule, "dtstart is required"), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidRrule, "dtstart is required"), request), response) } else { nextParameter := appcfg.ScheduledAnalysisParameter{ Enabled: true, diff --git a/cmd/api/src/api/tools/trace.go b/cmd/api/src/api/tools/trace.go index 10ce3bcda5..884bdca741 100644 --- a/cmd/api/src/api/tools/trace.go +++ b/cmd/api/src/api/tools/trace.go @@ -18,18 +18,16 @@ package tools import ( "bytes" + "errors" "fmt" "net/http" "net/url" "runtime/pprof" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/api" ) const ( - errUnknownProfile = errors.Error("unknown profile") - pprofEnableDebugSymbols = 1 profileQueryParameterName = "profile" @@ -42,6 +40,10 @@ const ( pprofLookupMutex = "mutex" ) +var ( + errUnknownProfile = errors.New("unknown profile") +) + func getProfileFromValues(values url.Values) (string, error) { // Default to looking up goroutine traces profile := pprofLookupGoroutines diff --git a/cmd/api/src/api/v2/ad_entity_test.go b/cmd/api/src/api/v2/ad_entity_test.go index 8b869c1bf8..55a053ecca 100644 --- a/cmd/api/src/api/v2/ad_entity_test.go +++ b/cmd/api/src/api/v2/ad_entity_test.go @@ -17,11 +17,11 @@ package v2_test import ( + "errors" "net/http" "testing" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/mediatypes" "github.com/specterops/bloodhound/src/api" 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 2e9a51626a..555177e8ea 100644 --- a/cmd/api/src/api/v2/ad_related_entity_test.go +++ b/cmd/api/src/api/v2/ad_related_entity_test.go @@ -17,11 +17,11 @@ package v2_test import ( + "errors" "net/http" "testing" "github.com/specterops/bloodhound/dawgs/ops" - "github.com/specterops/bloodhound/errors" v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/api/v2/apitest" dbMocks "github.com/specterops/bloodhound/src/database/mocks" diff --git a/cmd/api/src/api/v2/agi_test.go b/cmd/api/src/api/v2/agi_test.go index e94e6faead..27dd18bcff 100644 --- a/cmd/api/src/api/v2/agi_test.go +++ b/cmd/api/src/api/v2/agi_test.go @@ -19,6 +19,7 @@ package v2_test import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" @@ -28,7 +29,6 @@ import ( "github.com/gofrs/uuid" "github.com/gorilla/mux" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/graphschema/azure" "github.com/specterops/bloodhound/graphschema/common" diff --git a/cmd/api/src/api/v2/apiclient/apiclient.go b/cmd/api/src/api/v2/apiclient/apiclient.go index 1cacbd8588..55ec67813c 100644 --- a/cmd/api/src/api/v2/apiclient/apiclient.go +++ b/cmd/api/src/api/v2/apiclient/apiclient.go @@ -19,6 +19,7 @@ package apiclient import ( "bytes" "encoding/json" + "errors" "fmt" "io" "net" @@ -26,7 +27,6 @@ import ( "net/url" "time" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/mediatypes" diff --git a/cmd/api/src/api/v2/app_config_test.go b/cmd/api/src/api/v2/app_config_test.go index f91c73600e..7550e98064 100644 --- a/cmd/api/src/api/v2/app_config_test.go +++ b/cmd/api/src/api/v2/app_config_test.go @@ -19,20 +19,19 @@ package v2_test import ( "bytes" "encoding/json" + "errors" "fmt" "net/http" "net/http/httptest" "testing" - "go.uber.org/mock/gomock" - - "github.com/specterops/bloodhound/errors" v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/database/mocks" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/model/appcfg" "github.com/specterops/bloodhound/src/test/must" "github.com/specterops/bloodhound/src/utils/test" + "go.uber.org/mock/gomock" ) func Test_GetApplicationConfigurations(t *testing.T) { @@ -75,7 +74,7 @@ func Test_GetApplicationConfigurations(t *testing.T) { // Second call to GetAll should fail mockDB.EXPECT(). GetAllConfigurationParameters(gomock.Any()). - Return(nil, errors.Error("db error")) + Return(nil, errors.New("db error")) test.Request(t). WithMethod(http.MethodGet). @@ -100,7 +99,7 @@ func Test_GetApplicationConfigurations(t *testing.T) { mockDB.EXPECT(). GetConfigurationParameter(gomock.Any(), appcfg.PasswordExpirationWindow). - Return(appcfg.Parameter{}, errors.Error("db error")) + Return(appcfg.Parameter{}, errors.New("db error")) test.Request(t). WithMethod(http.MethodGet). diff --git a/cmd/api/src/api/v2/attack_path.go b/cmd/api/src/api/v2/attack_path.go index aae337251b..42cb615297 100644 --- a/cmd/api/src/api/v2/attack_path.go +++ b/cmd/api/src/api/v2/attack_path.go @@ -23,7 +23,7 @@ import ( const ( ErrorParseParams = "unable to parse request parameters" ErrorDecodeParams = "unable to decode request parameters" - ErrorNoDomainId = "no domain id specified in url" + ErrNoDomainId = "no domain id specified in url" ErrorNoFindingType = "no finding type specified" ErrorInvalidFindingType = "invalid finding type specified: %v" ErrorInvalidRFC3339 = "invalid RFC-3339 datetime format: %v" diff --git a/cmd/api/src/api/v2/auth/auth.go b/cmd/api/src/api/v2/auth/auth.go index 4bf96ec3b7..ea140119ac 100644 --- a/cmd/api/src/api/v2/auth/auth.go +++ b/cmd/api/src/api/v2/auth/auth.go @@ -45,10 +45,10 @@ import ( ) const ( - ErrorResponseDetailsNumRoles = "a user can only have one role" - ErrorResponseDetailsInvalidCurrentPassword = "unable to verify current password" - ErrorResponseDetailsMFAActivated = "multi-factor authentication already active" - ErrorResponseDetailsMFAEnrollmentRequired = "multi-factor authentication enrollment is required before activation" + ErrResponseDetailsNumRoles = "a user can only have one role" + ErrResponseDetailsInvalidCurrentPassword = "unable to verify current password" + ErrResponseDetailsMFAActivated = "multi-factor authentication already active" + ErrResponseDetailsMFAEnrollmentRequired = "multi-factor authentication enrollment is required before activation" ) type ManagementResource struct { @@ -300,7 +300,7 @@ func (s ManagementResource) CreateUser(response http.ResponseWriter, request *ht if err := api.ReadJSONRequestPayloadLimited(&createUserRequest, request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) } else if len(createUserRequest.Roles) > 1 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorResponseDetailsNumRoles, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrResponseDetailsNumRoles, request), response) } else if roles, err := s.db.GetRoles(request.Context(), createUserRequest.Roles); err != nil { api.HandleDatabaseError(request, response, err) } else { @@ -795,17 +795,17 @@ func (s ManagementResource) EnrollMFA(response http.ResponseWriter, request *htt } else if userId, err := uuid.FromString(rawUserId); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if err := api.ReadJSONRequestPayloadLimited(&payload, request); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorContentTypeJson.Error(), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrContentTypeJson.Error(), request), response) } else if user, err := s.db.GetUser(request.Context(), userId); err != nil { api.HandleDatabaseError(request, response, err) } else if user.SSOProviderID.Valid { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "Invalid operation, user is SSO", request), response) } else if user.AuthSecret.TOTPActivated { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorResponseDetailsMFAActivated, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrResponseDetailsMFAActivated, request), response) } else if err := api.ValidateSecret(s.secretDigester, payload.Secret, *user.AuthSecret); err != nil { // In this context an authenticated user revalidating their password for mfa enrollment should get a 400 bad request // b/c the bearer token is valid despite the secret in the request payload being invalid - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorResponseDetailsInvalidCurrentPassword, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrResponseDetailsInvalidCurrentPassword, request), response) } else if totpSecret, err := auth.GenerateTOTPSecret(host.String(), user.PrincipalName); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) } else { @@ -835,7 +835,7 @@ func (s ManagementResource) DisenrollMFA(response http.ResponseWriter, request * } else if userId, err := uuid.FromString(rawUserId); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if err := api.ReadJSONRequestPayloadLimited(&payload, request); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorContentTypeJson.Error(), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrContentTypeJson.Error(), request), response) } else if user, err := s.db.GetUser(request.Context(), userId); err != nil { api.HandleDatabaseError(request, response, err) } else { @@ -859,7 +859,7 @@ func (s ManagementResource) DisenrollMFA(response http.ResponseWriter, request * if err := api.ValidateSecret(s.secretDigester, payload.Secret, secretToValidate); err != nil { // In this context an authenticated user revalidating their password for mfa enrollment should get a 400 bad request // b/c the bearer token is valid despite the secret in the request payload being invalid - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorResponseDetailsInvalidCurrentPassword, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrResponseDetailsInvalidCurrentPassword, request), response) return } @@ -907,11 +907,11 @@ func (s ManagementResource) ActivateMFA(response http.ResponseWriter, request *h } else if userId, err := uuid.FromString(rawUserId); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsIDMalformed, request), response) } else if err := api.ReadJSONRequestPayloadLimited(&payload, request); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorContentTypeJson.Error(), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrContentTypeJson.Error(), request), response) } else if user, err := s.db.GetUser(request.Context(), userId); err != nil { api.HandleDatabaseError(request, response, err) } else if user.AuthSecret.TOTPSecret == "" { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorResponseDetailsMFAEnrollmentRequired, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrResponseDetailsMFAEnrollmentRequired, request), response) } else if !totp.Validate(payload.OTP, user.AuthSecret.TOTPSecret) { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsOTPInvalid, request), response) } else { diff --git a/cmd/api/src/api/v2/auth/auth_test.go b/cmd/api/src/api/v2/auth/auth_test.go index faef5730eb..81966be092 100644 --- a/cmd/api/src/api/v2/auth/auth_test.go +++ b/cmd/api/src/api/v2/auth/auth_test.go @@ -1053,7 +1053,7 @@ func TestCreateUser_Failure(t *testing.T) { }}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: auth.ErrorResponseDetailsNumRoles}}, + Errors: []api.ErrorDetails{{Message: auth.ErrResponseDetailsNumRoles}}, }, }, { @@ -2563,7 +2563,7 @@ func TestEnrollMFA(t *testing.T) { Input{userId.String(), "imnotjson"}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: api.ErrorContentTypeJson.Error()}}, + Errors: []api.ErrorDetails{{Message: api.ErrContentTypeJson.Error()}}, }, }, { @@ -2577,14 +2577,14 @@ func TestEnrollMFA(t *testing.T) { Input{activatedId.String(), nil}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: auth.ErrorResponseDetailsMFAActivated}}, + Errors: []api.ErrorDetails{{Message: auth.ErrResponseDetailsMFAActivated}}, }, }, { Input{badPassId.String(), nil}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: auth.ErrorResponseDetailsInvalidCurrentPassword}}, + Errors: []api.ErrorDetails{{Message: auth.ErrResponseDetailsInvalidCurrentPassword}}, }, }, { @@ -2648,7 +2648,7 @@ func TestDisenrollMFA_Failure(t *testing.T) { Input{userId.String(), "imnotjson"}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: api.ErrorContentTypeJson.Error()}}, + Errors: []api.ErrorDetails{{Message: api.ErrContentTypeJson.Error()}}, }, }, { @@ -2800,7 +2800,7 @@ func TestDisenrollMFA_Admin_FailureIncorrectPassword(t *testing.T) { *req, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: auth.ErrorResponseDetailsInvalidCurrentPassword}}, + Errors: []api.ErrorDetails{{Message: auth.ErrResponseDetailsInvalidCurrentPassword}}, }, ) } @@ -2954,7 +2954,7 @@ func TestActivateMFA_Failure(t *testing.T) { Input{unenrolledId.String(), "imnotjson"}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: api.ErrorContentTypeJson.Error()}}, + Errors: []api.ErrorDetails{{Message: api.ErrContentTypeJson.Error()}}, }, }, { @@ -2968,7 +2968,7 @@ func TestActivateMFA_Failure(t *testing.T) { Input{unenrolledId.String(), nil}, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: auth.ErrorResponseDetailsMFAEnrollmentRequired}}, + Errors: []api.ErrorDetails{{Message: auth.ErrResponseDetailsMFAEnrollmentRequired}}, }, }, } diff --git a/cmd/api/src/api/v2/auth/login.go b/cmd/api/src/api/v2/auth/login.go index 6db107fb03..1bbd14ed91 100644 --- a/cmd/api/src/api/v2/auth/login.go +++ b/cmd/api/src/api/v2/auth/login.go @@ -18,11 +18,11 @@ package auth import ( "context" + "errors" "fmt" "net/http" "strings" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" @@ -50,7 +50,7 @@ func (s LoginResource) loginSecret(loginRequest api.LoginRequest, response http. if loginDetails, err := s.authenticator.LoginWithSecret(request.Context(), loginRequest); err != nil { if errors.Is(err, api.ErrInvalidAuth) || errors.Is(err, api.ErrNoUserSecret) { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusUnauthorized, api.ErrorResponseDetailsAuthenticationInvalid, request), response) - } else if errors.Is(err, auth.ErrorInvalidOTP) { + } else if errors.Is(err, auth.ErrInvalidOTP) { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, api.ErrorResponseDetailsOTPInvalid, request), response) } else if errors.Is(err, api.ErrUserDisabled) { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusForbidden, err.Error(), request), response) diff --git a/cmd/api/src/api/v2/auth/login_internal_test.go b/cmd/api/src/api/v2/auth/login_internal_test.go index 9e32d6bffc..6b43a21970 100644 --- a/cmd/api/src/api/v2/auth/login_internal_test.go +++ b/cmd/api/src/api/v2/auth/login_internal_test.go @@ -84,7 +84,7 @@ func TestLoginFailure(t *testing.T) { } mockAuthenticator := api_mocks.NewMockAuthenticator(mockCtrl) - mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req1).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, auth.ErrorInvalidOTP) + mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req1).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, auth.ErrInvalidOTP) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req2).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, api.ErrInvalidAuth) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req3).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, fmt.Errorf("db error")) mockAuthenticator.EXPECT().LoginWithSecret(gomock.Any(), req4).Return(api.LoginDetails{User: model.User{EULAAccepted: true}}, api.ErrUserDisabled) diff --git a/cmd/api/src/api/v2/auth/saml_internal_test.go b/cmd/api/src/api/v2/auth/saml_internal_test.go index daa0d53aa5..5c48e299db 100644 --- a/cmd/api/src/api/v2/auth/saml_internal_test.go +++ b/cmd/api/src/api/v2/auth/saml_internal_test.go @@ -131,7 +131,7 @@ func TestAuth_CreateSSOSession(t *testing.T) { require.Equal(t, username, log.Fields["username"]) require.Equal(t, auth.ProviderTypeSAML, log.Fields["auth_type"]) if log.Status == model.AuditLogStatusFailure { - require.Equal(t, api.ErrorUserNotAuthorizedForProvider, log.Fields["error"]) + require.Equal(t, api.ErrUserNotAuthorizedForProvider, log.Fields["error"]) } }) @@ -153,7 +153,7 @@ func TestAuth_CreateSSOSession(t *testing.T) { require.Equal(t, username, log.Fields["username"]) require.Equal(t, auth.ProviderTypeSAML, log.Fields["auth_type"]) if log.Status == model.AuditLogStatusFailure { - require.Equal(t, api.ErrorUserNotAuthorizedForProvider.Error(), log.Fields["error"].(error).Error()) + require.Equal(t, api.ErrUserNotAuthorizedForProvider.Error(), log.Fields["error"].(error).Error()) } }) mockDB.EXPECT().LookupUser(gomock.Any(), username).Return(model.User{ diff --git a/cmd/api/src/api/v2/auth_integration_test.go b/cmd/api/src/api/v2/auth_integration_test.go index 6e301438c1..06d1d9f9ed 100644 --- a/cmd/api/src/api/v2/auth_integration_test.go +++ b/cmd/api/src/api/v2/auth_integration_test.go @@ -20,10 +20,10 @@ package v2_test import ( + "errors" "net/http" "testing" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/api/v2/integration" "github.com/specterops/bloodhound/src/auth" diff --git a/cmd/api/src/api/v2/azure.go b/cmd/api/src/api/v2/azure.go index 9bbb21a182..74f3ec6d37 100644 --- a/cmd/api/src/api/v2/azure.go +++ b/cmd/api/src/api/v2/azure.go @@ -18,6 +18,7 @@ package v2 import ( "context" + "errors" "fmt" "net/http" "sort" @@ -26,7 +27,6 @@ import ( "github.com/specterops/bloodhound/analysis/azure" "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/dawgs/ops" - "github.com/specterops/bloodhound/errors" azure2 "github.com/specterops/bloodhound/src/analysis/azure" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/api/bloodhoundgraph" @@ -42,11 +42,6 @@ const ( relatedEntityTypeQueryParameterName = "related_entity_type" relatedEntityReturnTypeQueryParameterName = "type" - errBadRelatedEntityReturnType = errors.Error("invalid return type requested for related entities") - errParameterRequired = errors.Error("missing required parameter") - errParameterSkip = errors.Error("invalid skip parameter") - errParameterRelatedEntityType = errors.Error("invalid related entity type") - entityTypeBase = "az-base" entityTypeUsers = "users" entityTypeGroups = "groups" @@ -69,6 +64,13 @@ const ( entityTypeFunctionApps = "function-apps" ) +var ( + errBadRelatedEntityReturnType = errors.New("invalid return type requested for related entities") + errParameterRequired = errors.New("missing required parameter") + errParameterSkip = errors.New("invalid skip parameter") + errParameterRelatedEntityType = errors.New("invalid related entity type") +) + func graphRelatedEntityType(ctx context.Context, db graph.Database, entityType, objectID string, request *http.Request) (any, int, *api.ErrorWrapper) { switch relatedEntityType := azure.RelatedEntityType(entityType); relatedEntityType { case azure.RelatedEntityTypeDescendentUsers, azure.RelatedEntityTypeDescendentGroups, diff --git a/cmd/api/src/api/v2/cypherquery.go b/cmd/api/src/api/v2/cypherquery.go index 9528b59fd9..bcef391b9e 100644 --- a/cmd/api/src/api/v2/cypherquery.go +++ b/cmd/api/src/api/v2/cypherquery.go @@ -17,10 +17,10 @@ package v2 import ( + "errors" "net/http" "github.com/specterops/bloodhound/dawgs/util" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" @@ -29,8 +29,8 @@ import ( "github.com/specterops/bloodhound/src/queries" ) -const ( - errUnauthorizedGraphMutation = errors.Error("unauthorized graph mutation") +var ( + errUnauthorizedGraphMutation = errors.New("unauthorized graph mutation") ) type CypherQueryPayload struct { diff --git a/cmd/api/src/api/v2/dataquality.go b/cmd/api/src/api/v2/dataquality.go index 795e60564e..c82b728cb8 100644 --- a/cmd/api/src/api/v2/dataquality.go +++ b/cmd/api/src/api/v2/dataquality.go @@ -31,9 +31,9 @@ import ( ) const ( - ErrorNoTenantId string = "no tenant id specified in url" - ErrorNoPlatformId string = "no platform id specified in url" - ErrorInvalidPlatformId string = "invalid platform id specified in url: %v" + ErrNoTenantId string = "no tenant id specified in url" + ErrNoPlatformId string = "no platform id specified in url" + ErrInvalidPlatformId string = "invalid platform id specified in url: %v" ) func (s Resources) GetDatabaseCompleteness(response http.ResponseWriter, request *http.Request) { @@ -90,7 +90,7 @@ func (s *Resources) GetADDataQualityStats(response http.ResponseWriter, request } if id, hasDomainID := mux.Vars(request)[api.URIPathVariableDomainID]; !hasDomainID { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorNoDomainId, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrNoDomainId, request), response) } else if start, err := ParseTimeQueryParameter(queryParams, "start", defaultStart); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRFC3339, queryParams["start"]), request), response) } else if end, err := ParseTimeQueryParameter(queryParams, "end", defaultEnd); err != nil { @@ -134,7 +134,7 @@ func (s *Resources) GetAzureDataQualityStats(response http.ResponseWriter, reque } if id, hasTenantID := mux.Vars(request)[api.URIPathVariableTenantID]; !hasTenantID { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorNoTenantId, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrNoTenantId, request), response) } else if start, err := ParseTimeQueryParameter(queryParams, "start", defaultStart); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRFC3339, queryParams["start"]), request), response) } else if end, err := ParseTimeQueryParameter(queryParams, "end", defaultEnd); err != nil { @@ -180,7 +180,7 @@ func (s *Resources) GetPlatformAggregateStats(response http.ResponseWriter, requ } if id, hasPlatformID := mux.Vars(request)[api.URIPathVariablePlatformID]; !hasPlatformID { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrorNoPlatformId, request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, ErrNoPlatformId, request), response) } else if start, err := ParseTimeQueryParameter(queryParams, "start", defaultStart); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidRFC3339, queryParams["start"]), request), response) } else if end, err := ParseTimeQueryParameter(queryParams, "end", defaultEnd); err != nil { @@ -209,7 +209,7 @@ func (s *Resources) GetPlatformAggregateStats(response http.ResponseWriter, requ return } default: - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrorInvalidPlatformId, id), request), response) + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf(ErrInvalidPlatformId, id), request), response) return } diff --git a/cmd/api/src/api/v2/dataquality_test.go b/cmd/api/src/api/v2/dataquality_test.go index 7db8d4e5fd..2b5adab570 100644 --- a/cmd/api/src/api/v2/dataquality_test.go +++ b/cmd/api/src/api/v2/dataquality_test.go @@ -542,7 +542,7 @@ func TestGetPlatformAggregateStats_Failure(t *testing.T) { }, api.ErrorWrapper{ HTTPStatus: http.StatusBadRequest, - Errors: []api.ErrorDetails{{Message: fmt.Sprintf(v2.ErrorInvalidPlatformId, "invalidPlatform")}}, + Errors: []api.ErrorDetails{{Message: fmt.Sprintf(v2.ErrInvalidPlatformId, "invalidPlatform")}}, }, }, // AD diff --git a/cmd/api/src/api/v2/file_uploads_test.go b/cmd/api/src/api/v2/file_uploads_test.go index 283f92f55c..1cca30890b 100644 --- a/cmd/api/src/api/v2/file_uploads_test.go +++ b/cmd/api/src/api/v2/file_uploads_test.go @@ -20,10 +20,10 @@ import ( "context" "database/sql" "encoding/json" + "errors" "net/http" "testing" - "github.com/specterops/bloodhound/errors" v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/api/v2/apitest" "github.com/specterops/bloodhound/src/auth" diff --git a/cmd/api/src/api/v2/flag_test.go b/cmd/api/src/api/v2/flag_test.go index 9500a33202..b55fecd43e 100644 --- a/cmd/api/src/api/v2/flag_test.go +++ b/cmd/api/src/api/v2/flag_test.go @@ -17,17 +17,16 @@ package v2_test import ( + "errors" "net/http" "testing" - "go.uber.org/mock/gomock" - - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/api" v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/database/mocks" "github.com/specterops/bloodhound/src/model/appcfg" "github.com/specterops/bloodhound/src/utils/test" + "go.uber.org/mock/gomock" ) func TestResources_GetFlags(t *testing.T) { @@ -51,7 +50,7 @@ func TestResources_GetFlags(t *testing.T) { Data: []appcfg.FeatureFlag{}, }) - mockDB.EXPECT().GetAllFlags(gomock.Any()).Return(nil, errors.Error("db error")) + mockDB.EXPECT().GetAllFlags(gomock.Any()).Return(nil, errors.New("db error")) test.Request(t). WithMethod(http.MethodGet). diff --git a/cmd/api/src/api/v2/helpers.go b/cmd/api/src/api/v2/helpers.go index 360f702b89..44c5fced30 100644 --- a/cmd/api/src/api/v2/helpers.go +++ b/cmd/api/src/api/v2/helpers.go @@ -17,6 +17,7 @@ package v2 import ( + "errors" "fmt" "net/http" "net/url" @@ -25,7 +26,6 @@ import ( "time" "github.com/gorilla/mux" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/utils" @@ -91,7 +91,7 @@ func ParseTimeQueryParameter(params url.Values, key string, defaultValue time.Ti func GetEntityObjectIDFromRequestPath(req *http.Request) (string, error) { if id, hasID := mux.Vars(req)["object_id"]; !hasID { - return "", errors.Error("no object ID found in request") + return "", errors.New("no object ID found in request") } else { return id, nil } diff --git a/cmd/api/src/api/v2/pathfinding_test.go b/cmd/api/src/api/v2/pathfinding_test.go index aea3d85680..ee2bb6e38f 100644 --- a/cmd/api/src/api/v2/pathfinding_test.go +++ b/cmd/api/src/api/v2/pathfinding_test.go @@ -17,11 +17,11 @@ package v2_test import ( + "errors" "net/http" "testing" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/src/api" v2 "github.com/specterops/bloodhound/src/api/v2" diff --git a/cmd/api/src/api/v2/search_test.go b/cmd/api/src/api/v2/search_test.go index 28dc04e1b8..d2c98c1d8e 100644 --- a/cmd/api/src/api/v2/search_test.go +++ b/cmd/api/src/api/v2/search_test.go @@ -17,19 +17,16 @@ package v2_test import ( + "errors" "fmt" "net/http" "testing" "github.com/specterops/bloodhound/dawgs/graph" - - "go.uber.org/mock/gomock" - - "github.com/specterops/bloodhound/errors" v2 "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/api/v2/apitest" - graphMocks "github.com/specterops/bloodhound/src/queries/mocks" + "go.uber.org/mock/gomock" ) func TestResources_SearchHandler(t *testing.T) { diff --git a/cmd/api/src/auth/model.go b/cmd/api/src/auth/model.go index 6bd0b6f5f0..819a09b4d7 100644 --- a/cmd/api/src/auth/model.go +++ b/cmd/api/src/auth/model.go @@ -20,6 +20,7 @@ import ( "context" "crypto/rand" "encoding/base64" + "errors" "fmt" "net/http" "strconv" @@ -27,7 +28,6 @@ import ( "github.com/gofrs/uuid" "github.com/golang-jwt/jwt/v4" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" diff --git a/cmd/api/src/auth/totp.go b/cmd/api/src/auth/totp.go index 22ff9ac800..3b4ead93a7 100644 --- a/cmd/api/src/auth/totp.go +++ b/cmd/api/src/auth/totp.go @@ -28,7 +28,7 @@ import ( ) var ( - ErrorInvalidOTP = fmt.Errorf("invalid one time password") + ErrInvalidOTP = fmt.Errorf("invalid one time password") ) func GenerateTOTPSecret(issuer, accountName string) (*otp.Key, error) { @@ -42,7 +42,7 @@ func ValidateTOTPSecret(otp string, secret model.AuthSecret) error { if !secret.TOTPActivated || totp.Validate(otp, secret.TOTPSecret) { return nil } else { - return ErrorInvalidOTP + return ErrInvalidOTP } } diff --git a/cmd/api/src/auth/totp_test.go b/cmd/api/src/auth/totp_test.go index e2ca251757..efc3ba8f10 100644 --- a/cmd/api/src/auth/totp_test.go +++ b/cmd/api/src/auth/totp_test.go @@ -64,7 +64,7 @@ func TestValidateTOTPSecret(t *testing.T) { }{ {Input{code, mfaSecret}, nil}, {Input{"", secret}, nil}, - {Input{"", mfaSecret}, auth.ErrorInvalidOTP}, + {Input{"", mfaSecret}, auth.ErrInvalidOTP}, } for _, tc := range cases { diff --git a/cmd/api/src/daemons/api/bhapi/api.go b/cmd/api/src/daemons/api/bhapi/api.go index 9f7b06d68e..1e1fa15df1 100644 --- a/cmd/api/src/daemons/api/bhapi/api.go +++ b/cmd/api/src/daemons/api/bhapi/api.go @@ -18,9 +18,9 @@ package bhapi import ( "context" + "errors" "net/http" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/config" ) diff --git a/cmd/api/src/daemons/api/toolapi/api.go b/cmd/api/src/daemons/api/toolapi/api.go index 66ac9ade3d..5ec6f75523 100644 --- a/cmd/api/src/daemons/api/toolapi/api.go +++ b/cmd/api/src/daemons/api/toolapi/api.go @@ -18,13 +18,13 @@ package toolapi import ( "context" + "errors" "net/http" "net/http/pprof" "github.com/go-chi/chi/v5" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/api/tools" diff --git a/cmd/api/src/daemons/datapipe/azure_convertors.go b/cmd/api/src/daemons/datapipe/azure_convertors.go index e84f60f8d0..c1373d84b1 100644 --- a/cmd/api/src/daemons/datapipe/azure_convertors.go +++ b/cmd/api/src/daemons/datapipe/azure_convertors.go @@ -191,7 +191,7 @@ func convertAzureAppOwner(raw json.RawMessage, converted *ConvertedAzureData) { ) if err := json.Unmarshal(raw.Owner, &owner); err != nil { log.Errorf(SerialError, "app owner", err) - } else if ownerType, err := ein.ExtractTypeFromDirectoryObject(owner); errors.Is(err, ein.InvalidTypeErr) { + } else if ownerType, err := ein.ExtractTypeFromDirectoryObject(owner); errors.Is(err, ein.ErrInvalidType) { log.Warnf(ExtractError, err) } else if err != nil { log.Errorf(ExtractError, err) @@ -238,7 +238,7 @@ func convertAzureDeviceOwner(raw json.RawMessage, converted *ConvertedAzureData) ) if err := json.Unmarshal(raw.Owner, &owner); err != nil { log.Errorf(SerialError, "device owner", err) - } else if ownerType, err := ein.ExtractTypeFromDirectoryObject(owner); errors.Is(err, ein.InvalidTypeErr) { + } else if ownerType, err := ein.ExtractTypeFromDirectoryObject(owner); errors.Is(err, ein.ErrInvalidType) { log.Warnf(ExtractError, err) } else if err != nil { log.Errorf(ExtractError, err) diff --git a/cmd/api/src/daemons/datapipe/jobs.go b/cmd/api/src/daemons/datapipe/jobs.go index 8a3836d368..29452fade5 100644 --- a/cmd/api/src/daemons/datapipe/jobs.go +++ b/cmd/api/src/daemons/datapipe/jobs.go @@ -143,12 +143,12 @@ func (s *Daemon) clearFileTask(ingestTask model.IngestTask) { // preProcessIngestFile will take a path and extract zips if necessary, returning the paths for files to process // along with any errors and the number of failed files (in the case of a zip archive) -func (s *Daemon) preProcessIngestFile(path string, fileType model.FileType) ([]string, error, int) { +func (s *Daemon) preProcessIngestFile(path string, fileType model.FileType) ([]string, int, error) { if fileType == model.FileTypeJson { //If this isn't a zip file, just return a slice with the path in it and let stuff process as normal - return []string{path}, nil, 0 + return []string{path}, 0, nil } else if archive, err := zip.OpenReader(path); err != nil { - return []string{}, err, 0 + return []string{}, 0, err } else { var ( errs = util.NewErrorCollector() @@ -164,7 +164,7 @@ func (s *Daemon) preProcessIngestFile(path string, fileType model.FileType) ([]s // Break out if temp file creation fails // Collect errors for other failures within the archive if tempFile, err := os.CreateTemp(s.cfg.TempDirectory(), "bh"); err != nil { - return []string{}, err, 0 + return []string{}, 0, err } else if srcFile, err := f.Open(); err != nil { errs.Add(fmt.Errorf("error opening file %s in archive %s: %v", f.Name, path, err)) failed++ @@ -189,7 +189,7 @@ func (s *Daemon) preProcessIngestFile(path string, fileType model.FileType) ([]s log.Errorf("Error deleting archive %s: %v", path, err) } - return filePaths, errs.Combined(), failed + return filePaths, failed, errs.Combined() } } @@ -202,7 +202,7 @@ func (s *Daemon) processIngestFile(ctx context.Context, path string, fileType mo } else { adcsEnabled = adcsFlag.Enabled } - if paths, err, failed := s.preProcessIngestFile(path, fileType); err != nil { + if paths, failed, err := s.preProcessIngestFile(path, fileType); err != nil { return 0, failed, err } else { failed = 0 diff --git a/cmd/api/src/database/audit.go b/cmd/api/src/database/audit.go index bb2c0007e8..418f0f1459 100644 --- a/cmd/api/src/database/audit.go +++ b/cmd/api/src/database/audit.go @@ -19,11 +19,11 @@ package database import ( "context" "database/sql" + "errors" "fmt" "time" "github.com/gofrs/uuid" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database/types" @@ -31,8 +31,8 @@ import ( "gorm.io/gorm" ) -const ( - ErrAuthContextInvalid = errors.Error("auth context is invalid") +var ( + ErrAuthContextInvalid = errors.New("auth context is invalid") ) func newAuditLog(context context.Context, entry model.AuditEntry, idResolver auth.IdentityResolver) (model.AuditLog, error) { diff --git a/cmd/api/src/database/auth.go b/cmd/api/src/database/auth.go index 8dc48ec748..46b465fbc5 100644 --- a/cmd/api/src/database/auth.go +++ b/cmd/api/src/database/auth.go @@ -22,17 +22,16 @@ import ( "context" "crypto/rand" "encoding/base64" + "errors" "fmt" "strings" "time" - "gorm.io/gorm" - "github.com/gofrs/uuid" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/database/types" "github.com/specterops/bloodhound/src/model" + "gorm.io/gorm" ) // NewClientAuthToken creates a new Client AuthToken row using the details provided @@ -512,7 +511,7 @@ func (s *BloodhoundDB) EndUserSession(ctx context.Context, userSession model.Use // corresponding retrival function is model.UserSession.GetFlag() func (s *BloodhoundDB) SetUserSessionFlag(ctx context.Context, userSession *model.UserSession, key model.SessionFlagKey, state bool) error { if userSession.ID == 0 { - return errors.Error("invalid session - missing session id") + return errors.New("invalid session - missing session id") } auditEntry := model.AuditEntry{} diff --git a/cmd/api/src/database/db.go b/cmd/api/src/database/db.go index 13e7dabd7a..292665c68c 100644 --- a/cmd/api/src/database/db.go +++ b/cmd/api/src/database/db.go @@ -20,27 +20,26 @@ package database import ( "context" + "errors" "fmt" "time" - "github.com/specterops/bloodhound/src/services/agi" - "github.com/specterops/bloodhound/src/services/dataquality" - "github.com/specterops/bloodhound/src/services/fileupload" - "github.com/specterops/bloodhound/src/services/ingest" - "github.com/gofrs/uuid" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/database/migration" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/model/appcfg" + "github.com/specterops/bloodhound/src/services/agi" + "github.com/specterops/bloodhound/src/services/dataquality" + "github.com/specterops/bloodhound/src/services/fileupload" + "github.com/specterops/bloodhound/src/services/ingest" "gorm.io/driver/postgres" "gorm.io/gorm" ) -const ( - ErrNotFound = errors.Error("entity not found") +var ( + ErrNotFound = errors.New("entity not found") ) var ( diff --git a/cmd/api/src/database/log.go b/cmd/api/src/database/log.go index 85f5f9cd86..2a5a3ccec5 100644 --- a/cmd/api/src/database/log.go +++ b/cmd/api/src/database/log.go @@ -18,11 +18,10 @@ package database import ( "context" + "errors" "time" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" - "gorm.io/gorm" "gorm.io/gorm/logger" ) diff --git a/cmd/api/src/database/types/null/int32_test.go b/cmd/api/src/database/types/null/int32_test.go index 69072db2f5..82fa4c9c57 100644 --- a/cmd/api/src/database/types/null/int32_test.go +++ b/cmd/api/src/database/types/null/int32_test.go @@ -18,11 +18,10 @@ package null import ( "encoding/json" + "errors" "math" "strconv" "testing" - - "github.com/specterops/bloodhound/errors" ) var ( diff --git a/cmd/api/src/database/types/object.go b/cmd/api/src/database/types/object.go index cdbc424c52..f3e864b9f4 100644 --- a/cmd/api/src/database/types/object.go +++ b/cmd/api/src/database/types/object.go @@ -19,9 +19,9 @@ package types import ( "database/sql/driver" "encoding/json" + "errors" "fmt" - "github.com/specterops/bloodhound/errors" "gorm.io/gorm" "gorm.io/gorm/schema" ) @@ -58,7 +58,7 @@ func (s *JSONBObject) Scan(value any) error { func (s *JSONBObject) Map(target any) error { if len(s.scannedBytes) == 0 { if s.Object == nil { - return errors.Error("JSONObject is nil") + return errors.New("JSONObject is nil") } if content, err := json.Marshal(s.Object); err != nil { diff --git a/cmd/api/src/model/filter.go b/cmd/api/src/model/filter.go index 0407e70bfb..01d7f34b57 100644 --- a/cmd/api/src/model/filter.go +++ b/cmd/api/src/model/filter.go @@ -17,6 +17,7 @@ package model import ( + "errors" "fmt" "net/http" "regexp" @@ -26,7 +27,6 @@ import ( "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/dawgs/query" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/common" ) @@ -53,10 +53,10 @@ const ( FalseString = "false" IdString = "id" ObjectIdString = "objectid" - - ErrNotFiltered = errors.Error("parameter value is not filtered") ) +var ErrNotFiltered = errors.New("parameter value is not filtered") + type Filtered interface { ValidFilters() map[string][]FilterOperator } diff --git a/cmd/api/src/model/ingest/ingest.go b/cmd/api/src/model/ingest/ingest.go index bd38d5655f..3ac3ad2259 100644 --- a/cmd/api/src/model/ingest/ingest.go +++ b/cmd/api/src/model/ingest/ingest.go @@ -18,9 +18,9 @@ package ingest import ( "encoding/json" + "errors" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/mediatypes" ) diff --git a/cmd/api/src/model/saved_queries.go b/cmd/api/src/model/saved_queries.go index 5ea317735e..e2ee4d2ab1 100644 --- a/cmd/api/src/model/saved_queries.go +++ b/cmd/api/src/model/saved_queries.go @@ -75,7 +75,7 @@ func (s SavedQueries) GetFilterableColumns() []string { func (s SavedQueries) GetValidFilterPredicatesAsStrings(column string) ([]string, error) { if predicates, validColumn := s.ValidFilters()[column]; !validColumn { - return []string{}, errors.New(ErrorResponseDetailsColumnNotFilterable) + return []string{}, errors.New(ErrResponseDetailsColumnNotFilterable) } else { var stringPredicates = make([]string, 0) for _, predicate := range predicates { diff --git a/cmd/api/src/model/search.go b/cmd/api/src/model/search.go index 3ae0bd088e..0ba0cfd3c7 100644 --- a/cmd/api/src/model/search.go +++ b/cmd/api/src/model/search.go @@ -76,7 +76,7 @@ func (s DomainSelectors) GetFilterableColumns() []string { func (s DomainSelectors) GetValidFilterPredicatesAsStrings(column string) ([]string, error) { if predicates, validColumn := s.ValidFilters()[column]; !validColumn { - return []string{}, errors.New(ErrorResponseDetailsColumnNotFilterable) + return []string{}, errors.New(ErrResponseDetailsColumnNotFilterable) } else { var stringPredicates = make([]string, 0) for _, predicate := range predicates { @@ -94,10 +94,10 @@ type OrderCriterion struct { type OrderCriteria []OrderCriterion const ( - ErrorResponseDetailsBadQueryParameterFilters = "there are errors in the query parameter filters specified" - ErrorResponseDetailsFilterPredicateNotSupported = "the specified filter predicate is not supported for this column" - ErrorResponseDetailsColumnNotFilterable = "the specified column cannot be filtered" - ErrorResponseDetailsColumnNotSortable = "the specified column cannot be sorted" + ErrResponseDetailsBadQueryParameterFilters = "there are errors in the query parameter filters specified" + ErrResponseDetailsFilterPredicateNotSupported = "the specified filter predicate is not supported for this column" + ErrResponseDetailsColumnNotFilterable = "the specified column cannot be filtered" + ErrResponseDetailsColumnNotSortable = "the specified column cannot be sorted" ) func (s DomainSelectors) GetOrderCriteria(params url.Values) (OrderCriteria, error) { @@ -118,7 +118,7 @@ func (s DomainSelectors) GetOrderCriteria(params url.Values) (OrderCriteria, err criterion.Property = column if !s.IsSortable(column) { - return OrderCriteria{}, errors.New(ErrorResponseDetailsColumnNotSortable) + return OrderCriteria{}, errors.New(ErrResponseDetailsColumnNotSortable) } orderCriteria = append(orderCriteria, criterion) @@ -133,18 +133,18 @@ func (s DomainSelectors) GetFilterCriteria(request *http.Request) (graph.Criteri ) if queryFilters, err := queryParameterFilterParser.ParseQueryParameterFilters(request); err != nil { - return nil, errors.New(ErrorResponseDetailsBadQueryParameterFilters) + return nil, errors.New(ErrResponseDetailsBadQueryParameterFilters) } else { for name, filters := range queryFilters { if valid := slices.Contains(s.GetFilterableColumns(), name); !valid { - return nil, errors.New(ErrorResponseDetailsColumnNotFilterable) + return nil, errors.New(ErrResponseDetailsColumnNotFilterable) } if validPredicates, err := s.GetValidFilterPredicatesAsStrings(name); err != nil { - return nil, errors.New(ErrorResponseDetailsColumnNotFilterable) + return nil, errors.New(ErrResponseDetailsColumnNotFilterable) } else { for i, filter := range filters { if !slices.Contains(validPredicates, string(filter.Operator)) { - return nil, errors.New(ErrorResponseDetailsFilterPredicateNotSupported) + return nil, errors.New(ErrResponseDetailsFilterPredicateNotSupported) } queryFilters[name][i].IsStringData = s.IsString(filter.Name) } diff --git a/cmd/api/src/model/search_test.go b/cmd/api/src/model/search_test.go index 205a3e1f2a..ca90d9e952 100644 --- a/cmd/api/src/model/search_test.go +++ b/cmd/api/src/model/search_test.go @@ -44,7 +44,7 @@ func TestDomainSelectors_GetFilterableColumns(t *testing.T) { func TestDomainSelectors_GetValidFilterPredicatesAsStrings(t *testing.T) { domains := DomainSelectors{} _, err := domains.GetValidFilterPredicatesAsStrings("foo") - require.Equal(t, ErrorResponseDetailsColumnNotFilterable, err.Error()) + require.Equal(t, ErrResponseDetailsColumnNotFilterable, err.Error()) columns := []string{"name", "objectid", "collected"} @@ -63,7 +63,7 @@ func TestDomainSelectors_GetOrderCriteria_InvalidSortColumn(t *testing.T) { params.Add("sort_by", "invalidColumn") _, err := domains.GetOrderCriteria(params) - require.Equal(t, ErrorResponseDetailsColumnNotSortable, err.Error()) + require.Equal(t, ErrResponseDetailsColumnNotSortable, err.Error()) } func TestDomainSelectors_GetOrderCriteria_Success(t *testing.T) { @@ -92,7 +92,7 @@ func TestDomainSelectors_GetFilterCriteria_InvalidFilterColumn(t *testing.T) { domains := DomainSelectors{} _, err = domains.GetFilterCriteria(request) - require.Equal(t, ErrorResponseDetailsColumnNotFilterable, err.Error()) + require.Equal(t, ErrResponseDetailsColumnNotFilterable, err.Error()) } func TestDomainSelectors_GetFilterCriteria_InvalidFilterPredicate(t *testing.T) { @@ -106,7 +106,7 @@ func TestDomainSelectors_GetFilterCriteria_InvalidFilterPredicate(t *testing.T) domains := DomainSelectors{} _, err = domains.GetFilterCriteria(request) - require.Equal(t, ErrorResponseDetailsFilterPredicateNotSupported, err.Error()) + require.Equal(t, ErrResponseDetailsFilterPredicateNotSupported, err.Error()) } func TestDomainSelectors_GetFilterCriteria_Success(t *testing.T) { diff --git a/cmd/api/src/model/sso_provider.go b/cmd/api/src/model/sso_provider.go index 2b93087ab8..61c64cfdac 100644 --- a/cmd/api/src/model/sso_provider.go +++ b/cmd/api/src/model/sso_provider.go @@ -39,10 +39,8 @@ func (s SSOProvider) AuditData() AuditData { switch s.Type { case SessionAuthProviderSAML: details = s.SAMLProvider - break case SessionAuthProviderOIDC: details = s.OIDCProvider - break } return AuditData{ diff --git a/cmd/api/src/queries/graph.go b/cmd/api/src/queries/graph.go index f525ce747d..465e9762ca 100644 --- a/cmd/api/src/queries/graph.go +++ b/cmd/api/src/queries/graph.go @@ -749,7 +749,7 @@ func (s *GraphQuery) ValidateOUs(ctx context.Context, ous []string) ([]string, e return nil }); err != nil { if graph.IsErrNotFound(err) { - return nil, errors.New(fmt.Sprintf("no record found for %s", ou)) + return nil, fmt.Errorf("no record found for %s", ou) } else { return nil, err } @@ -869,7 +869,7 @@ func (s *GraphQuery) runListQuery(ctx context.Context, node *graph.Node, params if result, err := s.runMaybeCachedEntityQuery(ctx, node, params, cacheEnabled); err != nil { return nil, 0, err } else if skip > result.Len() { - return nil, 0, errors.New(fmt.Sprintf(utils.ErrorInvalidSkip, skip)) + return nil, 0, fmt.Errorf(utils.ErrorInvalidSkip, skip) } else { if skip+limit > result.Len() { limit = result.Len() - skip diff --git a/cmd/api/src/queries/graph_internal_test.go b/cmd/api/src/queries/graph_internal_test.go index 9c8f2beb3d..1602e95b20 100644 --- a/cmd/api/src/queries/graph_internal_test.go +++ b/cmd/api/src/queries/graph_internal_test.go @@ -18,6 +18,7 @@ package queries import ( "context" + "errors" "fmt" "testing" "time" @@ -25,7 +26,6 @@ import ( "github.com/specterops/bloodhound/cache" "github.com/specterops/bloodhound/dawgs/graph" graph_mocks "github.com/specterops/bloodhound/dawgs/graph/mocks" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/src/model" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" diff --git a/cmd/api/src/utils/util.go b/cmd/api/src/utils/util.go index 74138685f5..e63a86b5a5 100644 --- a/cmd/api/src/utils/util.go +++ b/cmd/api/src/utils/util.go @@ -19,6 +19,7 @@ package utils import ( "context" "encoding/json" + "errors" "fmt" "net/http" "net/url" @@ -26,15 +27,16 @@ import ( "strconv" "strings" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/mediatypes" ) -var ErrInvalidSharpHoundVersion = errors.New("invalid sharphound version string") -var ErrInvalidAzureHoundVersion = errors.New("invalid azurehound version string") -var ErrRecommendSharphoundVersion = errors.New("please upgrade to sharphound v2.0.3 or above") -var ErrInvalidClientType = errors.New("invalid client type") +var ( + ErrInvalidSharpHoundVersion = errors.New("invalid sharphound version string") + ErrInvalidAzureHoundVersion = errors.New("invalid azurehound version string") + ErrRecommendSharphoundVersion = errors.New("please upgrade to sharphound v2.0.3 or above") + ErrInvalidClientType = errors.New("invalid client type") +) type ClientType int diff --git a/cmd/api/src/utils/validation/duration_validator.go b/cmd/api/src/utils/validation/duration_validator.go index 2ff8b8bde0..bfbf32e89a 100644 --- a/cmd/api/src/utils/validation/duration_validator.go +++ b/cmd/api/src/utils/validation/duration_validator.go @@ -68,10 +68,6 @@ func (s DurationValidator) okMax(d time.Duration) bool { return d <= s.maxD } -func (s DurationValidator) ok(lower, upper time.Duration) bool { - return s.okMin(lower) && s.okMax(upper) -} - func (s DurationValidator) Validate(value any) utils.Errors { var ( d time.Duration diff --git a/cmd/api/src/version/version.go b/cmd/api/src/version/version.go index f6069a2370..37b2b86714 100644 --- a/cmd/api/src/version/version.go +++ b/cmd/api/src/version/version.go @@ -18,16 +18,10 @@ package version import ( "encoding/json" + "errors" "fmt" "regexp" "strings" - - "github.com/specterops/bloodhound/errors" -) - -var ( - // See: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string - semverParsingRegex = regexp.MustCompile(`^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) ) const ( @@ -40,6 +34,14 @@ const ( prereleaseCaptureGroup = 4 ) +var ( + // See: https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string + semverParsingRegex = regexp.MustCompile(`^(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$`) + + ErrUnexpectedVersionFormat = errors.New("expected version to be formatted: ..[-]") + ErrMissingPrefix = fmt.Errorf("prefix `%s` is missing", Prefix) +) + type Version struct { Major int Minor int @@ -104,11 +106,11 @@ func (s Version) String() string { func Parse(rawVersion string) (Version, error) { if !strings.HasPrefix(rawVersion, Prefix) { - return Version{}, fmt.Errorf("version string %s does not start with the prefix %s", rawVersion, Prefix) + return Version{}, fmt.Errorf("%w: version string %s", ErrMissingPrefix, rawVersion) } if matches := semverParsingRegex.FindAllStringSubmatch(rawVersion[1:], 1); len(matches) != 1 { - return Version{}, errors.Error("expected version to be formatted: ..[-]") + return Version{}, ErrUnexpectedVersionFormat } else { // Map to the first set of capture groups versionParts := matches[0] diff --git a/go.work b/go.work index a24ed6fa52..ec312ddef2 100644 --- a/go.work +++ b/go.work @@ -26,7 +26,6 @@ use ( ./packages/go/cypher ./packages/go/dawgs ./packages/go/ein - ./packages/go/errors ./packages/go/graphschema ./packages/go/headers ./packages/go/lab diff --git a/packages/go/analysis/azure/azure.go b/packages/go/analysis/azure/azure.go index 2647df598f..35881ed687 100644 --- a/packages/go/analysis/azure/azure.go +++ b/packages/go/analysis/azure/azure.go @@ -17,14 +17,15 @@ package azure import ( + "errors" + "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/azure" ) -const ( - ErrNoNonEntityKindFound = errors.Error("unable to find a non-entity kind") - ErrInvalidRelatedEntityType = errors.Error("invalid related entity type") +var ( + ErrNoNonEntityKindFound = errors.New("unable to find a non-entity kind") + ErrInvalidRelatedEntityType = errors.New("invalid related entity type") ) func GetDescendentKinds(kind graph.Kind) []graph.Kind { diff --git a/packages/go/analysis/azure/post.go b/packages/go/analysis/azure/post.go index 41b7dd5867..5a2a5ce766 100644 --- a/packages/go/analysis/azure/post.go +++ b/packages/go/analysis/azure/post.go @@ -749,11 +749,7 @@ func resetPassword(operation analysis.StatTrackedOperation[analysis.CreatePostRe Kind: azure.ResetPassword, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) } } @@ -803,11 +799,7 @@ func globalAdmins(roleAssignments RoleAssignments, tenant *graph.Node, operation Kind: azure.GlobalAdmin, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) return nil @@ -825,11 +817,7 @@ func privilegedRoleAdmins(roleAssignments RoleAssignments, tenant *graph.Node, o Kind: azure.PrivilegedRoleAdmin, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) return nil @@ -847,11 +835,7 @@ func privilegedAuthAdmins(roleAssignments RoleAssignments, tenant *graph.Node, o Kind: azure.PrivilegedAuthAdmin, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) return nil @@ -875,11 +859,7 @@ func addMembers(roleAssignments RoleAssignments, operation analysis.StatTrackedO Kind: azure.AddMembers, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) return nil @@ -902,11 +882,7 @@ func addMembers(roleAssignments RoleAssignments, operation analysis.StatTrackedO Kind: azure.AddMembers, } - if !channels.Submit(ctx, outC, nextJob) { - return false - } - - return true + return !channels.Submit(ctx, outC, nextJob) }) } diff --git a/packages/go/crypto/argon2.go b/packages/go/crypto/argon2.go index 950dcff04f..18bff22804 100644 --- a/packages/go/crypto/argon2.go +++ b/packages/go/crypto/argon2.go @@ -19,6 +19,7 @@ package crypto import ( "crypto/rand" "encoding/base64" + "errors" "fmt" "regexp" "runtime" @@ -26,16 +27,12 @@ import ( "strings" "time" - "github.com/specterops/bloodhound/errors" - "github.com/shirou/gopsutil/v3/mem" - "golang.org/x/crypto/argon2" ) const ( SecretDigesterMethodArgon2 = "argon2" - ErrorMalformedArgon2Digest = errors.Error("argon2 digest is malformed") Argon2SaltByteLength = 16 Argon2DigestByteLength uint32 = 16 Argon2idVariant = "argon2id" @@ -68,7 +65,8 @@ const ( ) var ( - mcFormatRegex = regexp.MustCompile(mcFormatRegexPattern) + mcFormatRegex = regexp.MustCompile(mcFormatRegexPattern) + ErrMalformedArgon2Digest = errors.New("argon2 digest is malformed") ) type ComputerSpecs struct { @@ -111,7 +109,7 @@ func (s Argon2) ParseDigest(mcFormatDigest string) (SecretDigest, error) { var digest Argon2Digest if captureGroups := mcFormatRegex.FindStringSubmatch(mcFormatDigest); captureGroups == nil { - return digest, ErrorMalformedArgon2Digest + return digest, ErrMalformedArgon2Digest } else if version, err := strconv.ParseInt(captureGroups[mcFormatRegexVersionCapture], 10, 32); err != nil { return digest, err } else if memoryCost, err := strconv.ParseUint(captureGroups[mcFormatRegexMemoryCostCapture], 10, 32); err != nil { diff --git a/packages/go/cypher/models/cypher/copy.go b/packages/go/cypher/models/cypher/copy.go index 0662c6e44a..5a5b6eaf2c 100644 --- a/packages/go/cypher/models/cypher/copy.go +++ b/packages/go/cypher/models/cypher/copy.go @@ -144,6 +144,9 @@ func Copy[T any](value T, extensions ...CopyExtension[T]) T { case *Merge: return any(typedValue.copy()).(T) + case *MergeAction: + return any(typedValue.copy()).(T) + case *KindMatcher: return any(typedValue.copy()).(T) diff --git a/packages/go/cypher/models/pgsql/translate/delete.go b/packages/go/cypher/models/pgsql/translate/delete.go index daf7532a96..09fc46067b 100644 --- a/packages/go/cypher/models/pgsql/translate/delete.go +++ b/packages/go/cypher/models/pgsql/translate/delete.go @@ -49,9 +49,7 @@ func (s *Translator) translateDelete(scope *Scope, cypherDelete *cypher.Delete) if rewrittenProjections, err := buildProjection(typedExpression, identifierDeletion.UpdateBinding, scope); err != nil { return err } else { - for _, rewrittenProjection := range rewrittenProjections { - identifierDeletion.Projection = append(identifierDeletion.Projection, rewrittenProjection) - } + identifierDeletion.Projection = append(identifierDeletion.Projection, rewrittenProjections...) } } else { // Reflects this scope binding to the next query part diff --git a/packages/go/cypher/models/pgsql/translate/model.go b/packages/go/cypher/models/pgsql/translate/model.go index efbb0803d3..095691d80f 100644 --- a/packages/go/cypher/models/pgsql/translate/model.go +++ b/packages/go/cypher/models/pgsql/translate/model.go @@ -368,16 +368,3 @@ func extractIdentifierFromCypherExpression(expression cypher.Expression) (pgsql. return "", false, fmt.Errorf("unknown variable expression type: %T", variableExpression) } } - -func nodeJoinColumnsForPatternDirection(direction graph.Direction) (pgsql.Identifier, pgsql.Identifier, error) { - switch direction { - case graph.DirectionOutbound: - return pgsql.ColumnStartID, pgsql.ColumnEndID, nil - - case graph.DirectionInbound: - return pgsql.ColumnEndID, pgsql.ColumnStartID, nil - - default: - return "", "", fmt.Errorf("unsupported direction: %d", direction) - } -} diff --git a/packages/go/cypher/models/pgsql/translate/translator.go b/packages/go/cypher/models/pgsql/translate/translator.go index 8c15f52f9c..17ed170e01 100644 --- a/packages/go/cypher/models/pgsql/translate/translator.go +++ b/packages/go/cypher/models/pgsql/translate/translator.go @@ -108,10 +108,6 @@ func (s *Translator) pushState(state State) { s.state = append(s.state, state) } -func (s *Translator) popState() { - s.state = s.state[:len(s.state)-1] -} - func (s *Translator) exitState(expectedState State) { if currentState := s.currentState(); currentState != expectedState { s.SetErrorf("expected state %s but found %s", expectedState, currentState) @@ -120,16 +116,6 @@ func (s *Translator) exitState(expectedState State) { } } -func (s *Translator) inState(expectedState State) bool { - for _, state := range s.state { - if state == expectedState { - return true - } - } - - return false -} - func (s *Translator) Enter(expression cypher.SyntaxNode) { switch typedExpression := expression.(type) { case *cypher.RegularQuery, *cypher.SingleQuery, *cypher.PatternElement, *cypher.Return, diff --git a/packages/go/cypher/models/pgsql/translate/update.go b/packages/go/cypher/models/pgsql/translate/update.go index 6f5fc89ce9..8f0db897dd 100644 --- a/packages/go/cypher/models/pgsql/translate/update.go +++ b/packages/go/cypher/models/pgsql/translate/update.go @@ -43,9 +43,7 @@ func (s *Translator) translateUpdates(scope *Scope) error { if rewrittenProjections, err := buildProjection(identifierMutation.TargetBinding.Identifier, identifierMutation.UpdateBinding, scope); err != nil { return err } else { - for _, rewrittenProjection := range rewrittenProjections { - identifierMutation.Projection = append(identifierMutation.Projection, rewrittenProjection) - } + identifierMutation.Projection = append(identifierMutation.Projection, rewrittenProjections...) } continue diff --git a/packages/go/dawgs/drivers/pg/transaction.go b/packages/go/dawgs/drivers/pg/transaction.go index 184f6cf146..6cf6921fa4 100644 --- a/packages/go/dawgs/drivers/pg/transaction.go +++ b/packages/go/dawgs/drivers/pg/transaction.go @@ -270,7 +270,7 @@ func (s *transaction) Relationships() graph.RelationshipQuery { func (s *transaction) query(query string, parameters map[string]any) (pgx.Rows, error) { queryArgs := []any{s.queryExecMode, s.queryResultsFormat} - if parameters != nil && len(parameters) > 0 { + if len(parameters) > 0 { queryArgs = append(queryArgs, pgx.NamedArgs(parameters)) } diff --git a/packages/go/dawgs/graph/graph.go b/packages/go/dawgs/graph/graph.go index 27d6376788..5abfc0eb41 100644 --- a/packages/go/dawgs/graph/graph.go +++ b/packages/go/dawgs/graph/graph.go @@ -317,10 +317,10 @@ type Batch interface { // TODO: Existing batch logic expects this to perform an upsert on conficts with (start_id, end_id, kind). This is incorrect and should be refactored CreateRelationship(relationship *Relationship) error + // Deprecated: Use CreateRelationship Instead + // // CreateRelationshipByIDs creates a new Relationship from the start Node to the end Node with the given Kind and // Properties and returns the creation as a RelationshipResult. - // - // Deprecated: Use CreateRelationship CreateRelationshipByIDs(startNodeID, endNodeID ID, kind Kind, properties *Properties) error // DeleteRelationship deletes a relationship by the given ID. diff --git a/packages/go/dawgs/ops/parallel_test.go b/packages/go/dawgs/ops/parallel_test.go index 889c72d459..91696e95ba 100644 --- a/packages/go/dawgs/ops/parallel_test.go +++ b/packages/go/dawgs/ops/parallel_test.go @@ -18,13 +18,13 @@ package ops_test import ( "context" + "errors" "testing" "time" "github.com/specterops/bloodhound/dawgs/graph" graph_mocks "github.com/specterops/bloodhound/dawgs/graph/mocks" "github.com/specterops/bloodhound/dawgs/ops" - "github.com/specterops/bloodhound/errors" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) diff --git a/packages/go/dawgs/ops/traversal.go b/packages/go/dawgs/ops/traversal.go index c0bb08f49c..21bca9f387 100644 --- a/packages/go/dawgs/ops/traversal.go +++ b/packages/go/dawgs/ops/traversal.go @@ -17,12 +17,12 @@ package ops import ( + "errors" "fmt" "github.com/RoaringBitmap/roaring/roaring64" "github.com/specterops/bloodhound/dawgs/graph" "github.com/specterops/bloodhound/dawgs/query" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" ) diff --git a/packages/go/dawgs/traversal/id.go b/packages/go/dawgs/traversal/id.go index 8242c47568..680afff0c3 100644 --- a/packages/go/dawgs/traversal/id.go +++ b/packages/go/dawgs/traversal/id.go @@ -18,6 +18,7 @@ package traversal import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -26,7 +27,6 @@ import ( "github.com/specterops/bloodhound/dawgs/ops" "github.com/specterops/bloodhound/dawgs/util" "github.com/specterops/bloodhound/dawgs/util/channels" - "github.com/specterops/bloodhound/errors" ) // IDDriver is a function that drives sending queries to the graph and retrieving vertexes and edges. Traversal diff --git a/packages/go/dawgs/traversal/traversal.go b/packages/go/dawgs/traversal/traversal.go index db6acc36ba..c3245db5e4 100644 --- a/packages/go/dawgs/traversal/traversal.go +++ b/packages/go/dawgs/traversal/traversal.go @@ -18,6 +18,7 @@ package traversal import ( "context" + "errors" "fmt" "sync" "sync/atomic" @@ -30,7 +31,6 @@ import ( "github.com/specterops/bloodhound/dawgs/util" "github.com/specterops/bloodhound/dawgs/util/atomics" "github.com/specterops/bloodhound/dawgs/util/channels" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/log" ) diff --git a/packages/go/ein/azure.go b/packages/go/ein/azure.go index 8fb60a3ee6..5f9d7915ac 100644 --- a/packages/go/ein/azure.go +++ b/packages/go/ein/azure.go @@ -18,6 +18,7 @@ package ein import ( "encoding/json" + "errors" "fmt" "regexp" "slices" @@ -29,7 +30,6 @@ import ( "github.com/bloodhoundad/azurehound/v2/models" azure2 "github.com/bloodhoundad/azurehound/v2/models/azure" "github.com/specterops/bloodhound/dawgs/graph" - "github.com/specterops/bloodhound/errors" "github.com/specterops/bloodhound/graphschema/ad" "github.com/specterops/bloodhound/graphschema/azure" "github.com/specterops/bloodhound/graphschema/common" @@ -43,7 +43,7 @@ const ( var ( resourceGroupLevel = regexp.MustCompile(`^[\\w\\d\\-\\/]*/resourceGroups/[0-9a-zA-Z]+$`) - InvalidTypeErr = errors.New("invalid type returned from directory object") + ErrInvalidType = errors.New("invalid type returned from directory object") ) func ConvertAZAppToNode(app models.App) IngestibleNode { @@ -454,7 +454,7 @@ func ConvertAzureGroupMembersToRels(data models.GroupMembers) []IngestibleRelati ) if err := json.Unmarshal(raw.Member, &member); err != nil { log.Errorf(SerialError, "azure group member", err) - } else if memberType, err := ExtractTypeFromDirectoryObject(member); errors.Is(err, InvalidTypeErr) { + } else if memberType, err := ExtractTypeFromDirectoryObject(member); errors.Is(err, ErrInvalidType) { log.Warnf(ExtractError, err) } else if err != nil { log.Errorf(ExtractError, err) @@ -488,7 +488,7 @@ func ConvertAzureGroupOwnerToRels(data models.GroupOwners) []IngestibleRelations ) if err := json.Unmarshal(raw.Owner, &owner); err != nil { log.Errorf(SerialError, "azure group owner", err) - } else if ownerType, err := ExtractTypeFromDirectoryObject(owner); errors.Is(err, InvalidTypeErr) { + } else if ownerType, err := ExtractTypeFromDirectoryObject(owner); errors.Is(err, ErrInvalidType) { log.Warnf(ExtractError, err) } else if err != nil { log.Errorf(ExtractError, err) @@ -1074,7 +1074,7 @@ func ConvertAzureServicePrincipalOwnerToRels(data models.ServicePrincipalOwners) if err := json.Unmarshal(raw.Owner, &owner); err != nil { log.Errorf(SerialError, "azure service principal owner", err) - } else if ownerType, err := ExtractTypeFromDirectoryObject(owner); errors.Is(err, InvalidTypeErr) { + } else if ownerType, err := ExtractTypeFromDirectoryObject(owner); errors.Is(err, ErrInvalidType) { log.Warnf(ExtractError, err) } else if err != nil { log.Errorf(ExtractError, err) @@ -1871,7 +1871,7 @@ func ExtractTypeFromDirectoryObject(directoryObject azure2.DirectoryObject) (obj case enums.EntityDevice: return azure.Device, nil default: - return nil, fmt.Errorf("%w: %s", InvalidTypeErr, directoryObject.Type) + return nil, fmt.Errorf("%w: %s", ErrInvalidType, directoryObject.Type) } } diff --git a/packages/go/errors/error.go b/packages/go/errors/error.go deleted file mode 100644 index 44b1bc2849..0000000000 --- a/packages/go/errors/error.go +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 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 errors - -import ( - goerrors "errors" - "fmt" -) - -// Error is a type alias that implements the error interface. This allows a user to assign error compatible values as -// constants in a package. Being an immutable constant makes the resulting error value more suitable as a sentinel -// error. There are some minor compiler optimization benefits to this model as well. -type Error string - -// Error returns the string value of the error. -func (s Error) Error() string { - return string(s) -} - -// New is a type casting function for converting a string value into an Error value that implements the error interface. -func New(value string) error { - return Error(value) -} - -// Deprecated: Use stdlib errors.Is(...) instead. -// Is reports whether any error in err's chain matches target. -func Is(err error, target error) bool { - return goerrors.Is(err, target) -} - -// As finds the first error in err's chain that matches target, and if so, sets -// target to that error value and returns true. Otherwise, it returns false. -// -// The chain consists of err itself followed by the sequence of errors obtained by -// repeatedly calling Unwrap. -// -// An error matches target if the error's concrete value is assignable to the value -// pointed to by target, or if the error has a method As(interface{}) bool such that -// As(target) returns true. In the latter case, the As method is responsible for -// setting target. -// -// An error type might provide an As method so it can be treated as if it were a -// different error type. -// -// As panics if target is not a non-nil pointer to either a type that implements -// error, or to any interface type. -func As(err error, target any) bool { - return goerrors.As(err, target) -} - -// The ErrorCollector utilites are useful for aggregating errors across a multi-step -// process so that an early return is avoided and subsequent steps are allowed to execute. -// Ultimately, any errors that do happen can be returned as an aggregated list. -type ErrorCollector []error - -func (s *ErrorCollector) Return() error { - if s.HasErrors() { - return s - } - - return nil -} - -func (s *ErrorCollector) HasErrors() bool { - return s.Len() > 0 -} - -func (s *ErrorCollector) Len() int { - return len(*s) -} - -func (s *ErrorCollector) Collect(e error) { *s = append(*s, e) } - -func (s *ErrorCollector) Error() string { - err := "Collected errors:\n" - for i, e := range *s { - err += fmt.Sprintf("\tError %d: %s\n", i, e.Error()) - } - - return err -} diff --git a/packages/go/errors/error_test.go b/packages/go/errors/error_test.go deleted file mode 100644 index 7289ee169a..0000000000 --- a/packages/go/errors/error_test.go +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2023 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 errors - -import ( - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/require" -) - -func TestError_Error(t *testing.T) { - input := Error("test error") - output := input.Error() - - // output should have the same value and be a string type - require.Equal(t, "test error", output) - require.Equal(t, "string", reflect.TypeOf(output).Name()) -} - -func TestError_New(t *testing.T) { - output := New("test error") - - // output should have the same value and be an Error type - require.Equal(t, "test error", output.Error()) - require.Equal(t, "Error", reflect.TypeOf(output).Name()) -} - -func TestError_Is(t *testing.T) { - errTest := fmt.Errorf("test error") - err := errTest - require.True(t, Is(err, errTest)) -} - -func TestError_As(t *testing.T) { - err := fmt.Errorf("test error") - errTest2 := fmt.Errorf("this should go away") - - require.True(t, As(err, &errTest2)) - require.Equal(t, err, errTest2) -} diff --git a/packages/go/errors/go.mod b/packages/go/errors/go.mod deleted file mode 100644 index 1d5e501bd8..0000000000 --- a/packages/go/errors/go.mod +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2023 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 - -module github.com/specterops/bloodhound/errors - -go 1.23 - -require github.com/stretchr/testify v1.9.0 - -require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.13.1 // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/packages/go/errors/go.sum b/packages/go/errors/go.sum deleted file mode 100644 index 1ececf5ff8..0000000000 --- a/packages/go/errors/go.sum +++ /dev/null @@ -1,21 +0,0 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7c1676effd612aed45008f06ffd2fef32fe2c87c Mon Sep 17 00:00:00 2001 From: Arianna Cooper Date: Thu, 2 Jan 2025 12:00:26 -0800 Subject: [PATCH 6/9] BED-5070: Add Auto-provisioning Support to SSO Auth Flow on Backend (#1025) --- cmd/api/src/api/registration/registration.go | 2 +- cmd/api/src/api/v2/auth/oidc.go | 221 +++++++---- cmd/api/src/api/v2/auth/oidc_test.go | 103 +++++- cmd/api/src/api/v2/auth/saml.go | 326 ++++++++++------ cmd/api/src/api/v2/auth/sso.go | 32 +- cmd/api/src/api/v2/auth/sso_test.go | 257 ++++--------- cmd/api/src/database/auth_test.go | 286 +++++++++----- .../database/migration/migrations/v6.3.0.sql | 2 +- .../database/migration/migrations/v6.4.0.sql | 21 ++ cmd/api/src/database/mocks/db.go | 24 +- cmd/api/src/database/oidc_providers.go | 6 +- cmd/api/src/database/oidc_providers_test.go | 70 ++-- cmd/api/src/database/samlproviders.go | 6 +- cmd/api/src/database/sso_providers.go | 23 +- cmd/api/src/database/sso_providers_test.go | 350 +++++++++++++++++- cmd/api/src/model/samlprovider.go | 28 +- cmd/api/src/model/sso_provider.go | 41 +- packages/go/openapi/doc/openapi.json | 86 ++++- .../src/paths/auth.sso-providers.saml.yaml | 19 + .../src/paths/sso.sso-providers.id.yaml | 28 ++ .../src/paths/sso.sso-providers.oidc.yaml | 20 + 21 files changed, 1409 insertions(+), 542 deletions(-) create mode 100644 cmd/api/src/database/migration/migrations/v6.4.0.sql diff --git a/cmd/api/src/api/registration/registration.go b/cmd/api/src/api/registration/registration.go index 39e9ebb624..8e60f2b463 100644 --- a/cmd/api/src/api/registration/registration.go +++ b/cmd/api/src/api/registration/registration.go @@ -52,7 +52,7 @@ func RegisterFossGlobalMiddleware(routerInst *router.Router, cfg config.Configur func RegisterFossRoutes( routerInst *router.Router, cfg config.Configuration, - rdms *database.BloodhoundDB, + rdms database.Database, graphDB *graph.DatabaseSwitch, graphQuery queries.Graph, apiCache cache.Cache, diff --git a/cmd/api/src/api/v2/auth/oidc.go b/cmd/api/src/api/v2/auth/oidc.go index a76b1da00c..3cf32b1ede 100644 --- a/cmd/api/src/api/v2/auth/oidc.go +++ b/cmd/api/src/api/v2/auth/oidc.go @@ -17,67 +17,105 @@ package auth import ( + "context" "errors" "fmt" "net/http" + "net/url" "time" - "github.com/specterops/bloodhound/log" - "github.com/coreos/go-oidc/v3/oidc" "github.com/specterops/bloodhound/headers" + "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/config" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" + "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/utils/validation" "golang.org/x/oauth2" ) +var ( + ErrOIDCProviderMissing = errors.New("oidc provider missing") + ErrOIDCIssuerURLInvalid = errors.New("oidc provider issuer url invalid") + ErrRoleIDInvalid = errors.New("role id invalid") + ErrEmailClaimMissing = errors.New("") +) + +type oidcClaims struct { + Name string `json:"name"` + FamilyName string `json:"family_name"` + DisplayName string `json:"given_name"` + Email string `json:"email"` + Verified bool `json:"email_verified"` +} + // 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"` + Name string `json:"name" validate:"required"` + Issuer string `json:"issuer" validate:"url"` + ClientID string `json:"client_id" validate:"required"` + Config *model.SSOProviderConfig `json:"config,omitempty"` } // 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 { + if err := api.ReadJSONRequestPayloadLimited(&upsertReq, request); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if ssoProvider, err := updateOIDCProvider(request.Context(), ssoProvider, upsertReq, s.db); errors.Is(err, ErrOIDCProviderMissing) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) + } else if errors.Is(err, ErrOIDCIssuerURLInvalid) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "issuer url is invalid", request), response) + } else if errors.Is(err, ErrRoleIDInvalid) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "role id is invalid", request), response) + } else if err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusInternalServerError, api.ErrorResponseDetailsInternalServerError, request), response) + } else if oidcProvider, err := s.db.UpdateOIDCProvider(request.Context(), ssoProvider); errors.Is(err, database.ErrDuplicateSSOProviderName) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) + } else if err != nil { + api.HandleDatabaseError(request, response, err) } else { - if upsertReq.Name != "" { - ssoProvider.Name = upsertReq.Name - } + api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusOK, response) + } +} - if upsertReq.ClientID != "" { - ssoProvider.OIDCProvider.ClientID = upsertReq.ClientID - } +func updateOIDCProvider(ctx context.Context, ssoProvider model.SSOProvider, upsertReq UpsertOIDCProviderRequest, r getRoler) (model.SSOProvider, error) { + if ssoProvider.OIDCProvider == nil { + return ssoProvider, ErrOIDCProviderMissing + } - 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 - } + if upsertReq.Name != "" { + ssoProvider.Name = upsertReq.Name + } + + if upsertReq.ClientID != "" { + ssoProvider.OIDCProvider.ClientID = upsertReq.ClientID + } - ssoProvider.OIDCProvider.Issuer = upsertReq.Issuer + if upsertReq.Issuer != "" { + if _, err := url.ParseRequestURI(upsertReq.Issuer); err != nil { + return ssoProvider, ErrOIDCIssuerURLInvalid } - if oidcProvider, err := s.db.UpdateOIDCProvider(request.Context(), ssoProvider); err != nil { - if errors.Is(err, database.ErrDuplicateSSOProviderName) { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) - } else { - api.HandleDatabaseError(request, response, err) - } + ssoProvider.OIDCProvider.Issuer = upsertReq.Issuer + } + + // Need to ensure that if no config is specified, we don't accidentally wipe the existing configuration + if upsertReq.Config != nil { + if !upsertReq.Config.AutoProvision.Enabled { + ssoProvider.Config.AutoProvision = model.SSOProviderAutoProvisionConfig{} + } else if _, err := r.GetRole(ctx, upsertReq.Config.AutoProvision.DefaultRoleId); err != nil { + return ssoProvider, ErrRoleIDInvalid } else { - api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusOK, response) + ssoProvider.Config.AutoProvision = upsertReq.Config.AutoProvision } } + + return ssoProvider, nil } // CreateOIDCProvider creates an OIDC provider entry given a valid request @@ -88,25 +126,26 @@ 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(upsertReq); validated != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, validated.Error(), request), response) + } else if upsertReq.Config == nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "config is required", request), response) + } else if _, err := s.db.GetRole(request.Context(), upsertReq.Config.AutoProvision.DefaultRoleId); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "role id is invalid", request), response) + } else if oidcProvider, err := s.db.CreateOIDCProvider(request.Context(), upsertReq.Name, upsertReq.Issuer, upsertReq.ClientID, *upsertReq.Config); errors.Is(err, database.ErrDuplicateSSOProviderName) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) + } else if err != nil { + api.HandleDatabaseError(request, response, err) } else { - if oidcProvider, err := s.db.CreateOIDCProvider(request.Context(), upsertReq.Name, upsertReq.Issuer, upsertReq.ClientID); err != nil { - if errors.Is(err, database.ErrDuplicateSSOProviderName) { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) - } else { - api.HandleDatabaseError(request, response, err) - } - } else { - api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusCreated, response) - } + api.WriteBasicResponse(request.Context(), oidcProvider, http.StatusCreated, response) } } -func getRedirectURL(request *http.Request, provider model.SSOProvider) string { - hostUrl := *ctx.FromRequest(request).Host +func getRedirectURL(hostUrl url.URL, provider model.SSOProvider) string { return fmt.Sprintf("%s/api/v2/sso/%s/callback", hostUrl.String(), provider.Slug) } func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { + var hostURL = *ctx.Get(request.Context()).Host + if ssoProvider.OIDCProvider == nil { // SSO misconfiguration scenario redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") @@ -123,7 +162,7 @@ func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, reque conf := &oauth2.Config{ ClientID: ssoProvider.OIDCProvider.ClientID, Endpoint: provider.Endpoint(), - RedirectURL: getRedirectURL(request, ssoProvider), + RedirectURL: getRedirectURL(hostURL, ssoProvider), Scopes: []string{"openid", "profile", "email"}, } @@ -172,44 +211,76 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re log.Errorf("[OIDC] Failed to create OIDC provider: %v", err) // SSO misconfiguration scenario redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + } else if claims, err := getOIDCClaims(request.Context(), provider, ssoProvider, pkceVerifier, code[0]); err != nil { + log.Errorf("[OIDC] %v", err) + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { - var ( - oidcVerifier = provider.Verifier(&oidc.Config{ClientID: ssoProvider.OIDCProvider.ClientID}) - oauth2Conf = &oauth2.Config{ - ClientID: ssoProvider.OIDCProvider.ClientID, - Endpoint: provider.Endpoint(), - RedirectURL: getRedirectURL(request, ssoProvider), // Required as verification check - } - ) - - if token, err := oauth2Conf.Exchange(request.Context(), code[0], oauth2.VerifierOption(pkceVerifier.Value)); err != nil { - log.Errorf("[OIDC] Token exchange failed: %v", err) - // SAML credentials issue equivalent for OIDC authentication - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") - } else if rawIDToken, ok := token.Extra("id_token").(string); !ok { // Extract the ID Token from OAuth2 token - // Missing ID token - credentials issue - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") - } else if idToken, err := oidcVerifier.Verify(request.Context(), rawIDToken); err != nil { - log.Errorf("[OIDC] ID token verification failed: %v", err) - // Credentials issue scenario - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") - } else { - // Extract custom claims - var claims struct { - Name string `json:"name"` - FamilyName string `json:"family_name"` - DisplayName string `json:"given_name"` - Email string `json:"email"` - Verified bool `json:"email_verified"` - } - if err := idToken.Claims(&claims); err != nil { - log.Errorf("[OIDC] Failed to parse claims: %v", err) - // Technical or credentials issue - // Not explicitly covered; treat as a technical issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") - } else { - s.authenticator.CreateSSOSession(request, response, claims.Email, ssoProvider) + if ssoProvider.Config.AutoProvision.Enabled { + if err := jitOIDCUserCreation(request.Context(), ssoProvider, claims, s.db); err != nil { + // It is safe to let this request drop into the CreateSSOSession function below to ensure proper audit logging + log.Errorf("[OIDC] Error during JIT User Creation: %v", err) } } + + s.authenticator.CreateSSOSession(request, response, claims.Email, ssoProvider) } } + +func getOIDCClaims(reqCtx context.Context, provider *oidc.Provider, ssoProvider model.SSOProvider, pkceVerifier *http.Cookie, code string) (oidcClaims, error) { + var ( + hostURL = *ctx.Get(reqCtx).Host + oidcVerifier = provider.Verifier(&oidc.Config{ClientID: ssoProvider.OIDCProvider.ClientID}) + oauth2Conf = &oauth2.Config{ + ClientID: ssoProvider.OIDCProvider.ClientID, + Endpoint: provider.Endpoint(), + RedirectURL: getRedirectURL(hostURL, ssoProvider), // Required as verification check + } + claims = oidcClaims{} + ) + + if token, err := oauth2Conf.Exchange(reqCtx, code, oauth2.VerifierOption(pkceVerifier.Value)); err != nil { + return claims, fmt.Errorf("id token exchange: %v", err) + } else if rawIDToken, ok := token.Extra("id_token").(string); !ok { // Extract the ID Token from OAuth2 token + return claims, fmt.Errorf("id token missing key id_token: %v", err) + } else if idToken, err := oidcVerifier.Verify(reqCtx, rawIDToken); err != nil { + return claims, fmt.Errorf("id token verification: %v", err) + } else if err := idToken.Claims(&claims); err != nil { + return claims, fmt.Errorf("parse claims: %v", err) + } else if claims.Email == "" { + return claims, ErrEmailClaimMissing + } else { + return claims, nil + } +} + +func jitOIDCUserCreation(ctx context.Context, ssoProvider model.SSOProvider, claims oidcClaims, u jitUserCreator) error { + if role, err := u.GetRole(ctx, ssoProvider.Config.AutoProvision.DefaultRoleId); err != nil { + return fmt.Errorf("get role: %v", err) + } else if _, err := u.LookupUser(ctx, claims.Email); err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("lookup user: %v", err) + } else if errors.Is(err, database.ErrNotFound) { + var user = model.User{ + EmailAddress: null.StringFrom(claims.Email), + PrincipalName: claims.Email, + Roles: model.Roles{role}, + SSOProviderID: null.Int32From(ssoProvider.ID), + EULAAccepted: true, // EULA Acceptance does not pertain to Bloodhound Community Edition; this flag is used for Bloodhound Enterprise users + FirstName: null.StringFrom(claims.Email), + LastName: null.StringFrom("Last name not found"), + } + + if claims.DisplayName != "" { + user.FirstName = null.StringFrom(claims.DisplayName) + } + + if claims.FamilyName != "" { + user.LastName = null.StringFrom(claims.FamilyName) + } + + if _, err := u.CreateUser(ctx, user); err != nil { + return fmt.Errorf("create user: %v", err) + } + } + + return nil +} diff --git a/cmd/api/src/api/v2/auth/oidc_test.go b/cmd/api/src/api/v2/auth/oidc_test.go index 72c88982ac..ee50d5a566 100644 --- a/cmd/api/src/api/v2/auth/oidc_test.go +++ b/cmd/api/src/api/v2/auth/oidc_test.go @@ -37,7 +37,8 @@ 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(), "Bloodhound gang", "https://localhost/auth", "bloodhound").Return(model.OIDCProvider{ + mockDB.EXPECT().GetRole(gomock.Any(), int32(0)).Return(model.Role{}, nil) + mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "Bloodhound gang", "https://localhost/auth", "bloodhound", model.SSOProviderConfig{}).Return(model.OIDCProvider{ ClientID: "bloodhound", Issuer: "https://localhost/auth", }, nil) @@ -47,12 +48,61 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) { Name: "Bloodhound gang", Issuer: "https://localhost/auth", ClientID: "bloodhound", + Config: &model.SSOProviderConfig{}, }). OnHandlerFunc(resources.CreateOIDCProvider). Require(). ResponseStatusCode(http.StatusCreated) }) + t.Run("successfully create a new OIDCProvider with config values", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + mockDB.EXPECT().GetRole(gomock.Any(), int32(3)).Return(model.Role{Serial: model.Serial{ID: 3}}, nil) + mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "Bloodhound gang2", "https://localhost/auth", "bloodhound", config).Return(model.OIDCProvider{ + ClientID: "bloodhound", + Issuer: "https://localhost/auth", + }, nil) + + test.Request(t). + WithBody(auth.UpsertOIDCProviderRequest{ + Name: "Bloodhound gang2", + Issuer: "https://localhost/auth", + ClientID: "bloodhound", + Config: &config, + }). + OnHandlerFunc(resources.CreateOIDCProvider). + Require(). + ResponseStatusCode(http.StatusCreated) + }) + + t.Run("error invalid role id", func(t *testing.T) { + mockDB.EXPECT().GetRole(gomock.Any(), int32(7)).Return(model.Role{Serial: model.Serial{ID: 7}}, fmt.Errorf("role id is invalid")) + + test.Request(t). + WithBody(auth.UpsertOIDCProviderRequest{ + Name: "Gotham Net 2", + Issuer: "https://gotham-2.net", + ClientID: "gotham-net-2", + Config: &model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 7, + RoleProvision: true, + }, + }, + }). + OnHandlerFunc(resources.CreateOIDCProvider). + Require(). + ResponseStatusCode(http.StatusBadRequest) + }) + t.Run("error parsing body request", func(t *testing.T) { test.Request(t). OnHandlerFunc(resources.CreateOIDCProvider). @@ -84,13 +134,15 @@ func TestManagementResource_CreateOIDCProvider(t *testing.T) { }) 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")) + mockDB.EXPECT().GetRole(gomock.Any(), int32(0)).Return(model.Role{}, nil) + mockDB.EXPECT().CreateOIDCProvider(gomock.Any(), "test", "https://localhost/auth", "bloodhound", model.SSOProviderConfig{}).Return(model.OIDCProvider{}, fmt.Errorf("error")) test.Request(t). WithBody(auth.UpsertOIDCProviderRequest{ Name: "test", Issuer: "https://localhost/auth", ClientID: "bloodhound", + Config: &model.SSOProviderConfig{}, }). OnHandlerFunc(resources.CreateOIDCProvider). Require(). @@ -130,6 +182,53 @@ func TestManagementResource_UpdateOIDCProvider(t *testing.T) { ResponseStatusCode(http.StatusOK) }) + t.Run("successfully update an OIDCProvider with config values", func(t *testing.T) { + mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil) + mockDB.EXPECT().GetRole(gomock.Any(), int32(3)).Return(model.Role{Serial: model.Serial{ID: 3}}, 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", + Config: &model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + }, + }). + OnHandlerFunc(resources.UpdateSSOProvider). + Require(). + ResponseStatusCode(http.StatusOK) + }) + + t.Run("error invalid role id", func(t *testing.T) { + mockDB.EXPECT().GetSSOProviderById(gomock.Any(), int32(1)).Return(baseProvider, nil) + mockDB.EXPECT().GetRole(gomock.Any(), int32(7)).Return(model.Role{Serial: model.Serial{ID: 7}}, fmt.Errorf("role id is invalid")) + + test.Request(t). + WithURLPathVars(urlParams). + WithBody(auth.UpsertOIDCProviderRequest{ + Name: "Gotham Net 2", + Issuer: "https://gotham-2.net", + ClientID: "gotham-net-2", + Config: &model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 7, + RoleProvision: true, + }, + }, + }). + OnHandlerFunc(resources.UpdateSSOProvider). + Require(). + ResponseStatusCode(http.StatusBadRequest) + }) + 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) diff --git a/cmd/api/src/api/v2/auth/saml.go b/cmd/api/src/api/v2/auth/saml.go index 6db5b0f8a5..70aab79af2 100644 --- a/cmd/api/src/api/v2/auth/saml.go +++ b/cmd/api/src/api/v2/auth/saml.go @@ -17,10 +17,12 @@ package auth import ( + "context" "encoding/xml" "errors" "fmt" "io" + "mime/multipart" "net/http" "strconv" "strings" @@ -37,6 +39,7 @@ import ( "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" + "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" ) @@ -52,6 +55,89 @@ const ( ` ) +var ErrSAMLProviderMissing = errors.New("saml provider missing") + +func getMetadataXML(fileHeader *multipart.FileHeader) ([]byte, error) { + reader, err := fileHeader.Open() + if err != nil { + return nil, fmt.Errorf("xml file could not be opened: %v", err) + } + + defer reader.Close() + + metadataXML, err := io.ReadAll(reader) + if err != nil { + return metadataXML, fmt.Errorf("xml file could not be read: %v", err) + } + + return metadataXML, nil +} + +func getMetadataFromMultipartRequest(multipartForm *multipart.Form, isRequired bool) ([]byte, *saml.EntityDescriptor, error) { + if metadataXMLFileHandles, hasMetadataXML := multipartForm.File["metadata"]; !hasMetadataXML { + if isRequired { + return nil, nil, fmt.Errorf("form is missing \"metadata\" parameter") + } + return nil, nil, nil + } else if numHeaders := len(metadataXMLFileHandles); numHeaders == 0 || numHeaders > 1 { + return nil, nil, fmt.Errorf("expected only one \"metadata\" parameter") + } else if metadataXML, err := getMetadataXML(metadataXMLFileHandles[0]); err != nil { + return nil, nil, err + } else if metadata, err := samlsp.ParseMetadata(metadataXML); err != nil { + return nil, nil, err + } else { + return metadataXML, metadata, nil + } +} + +func getProviderNameFromMultipartRequest(multipartForm *multipart.Form, isRequired bool) (string, error) { + if providerNames, hasProviderName := multipartForm.Value["name"]; !hasProviderName { + if isRequired { + return "", fmt.Errorf("form is missing \"name\" parameter") + } + return "", nil + } else if numProviderNames := len(providerNames); numProviderNames == 0 || numProviderNames > 1 { + return "", fmt.Errorf("expected only one \"name\" parameter") + } else { + return providerNames[0], nil + } +} + +func getSSOProviderConfigFromMultipartRequest(ctx context.Context, multipartForm *multipart.Form, isRequired bool, r getRoler) (*model.SSOProviderConfig, error) { + if autoProvisionEnabled, hasAutoProvisionEnabled := multipartForm.Value["config.auto_provision.enabled"]; !hasAutoProvisionEnabled { + if isRequired { + return nil, fmt.Errorf("form is missing \"config.auto_provision.enabled\" parameter") + } + return nil, nil + } else if len(autoProvisionEnabled) > 1 { + return nil, fmt.Errorf("expected only one \"config.auto_provision.enabled\" parameter") + } else if isAutoProvisionEnabled, err := strconv.ParseBool(autoProvisionEnabled[0]); err != nil { + return nil, fmt.Errorf("\"config.auto_provision.enabled\" parameter could not be converted to bool") + } else if defaultRoleId, hasDefaultRoleId := multipartForm.Value["config.auto_provision.default_role_id"]; !hasDefaultRoleId { + return nil, fmt.Errorf("form is missing \"config.auto_provision.default_role_id\" parameter") + } else if len(defaultRoleId) > 1 { + return nil, fmt.Errorf("\"config.auto_provision.default_role_id\" has more than one value") + } else if defaultRoleIdInt, err := strconv.Atoi(defaultRoleId[0]); err != nil { + return nil, fmt.Errorf("\"config.auto_provision.default_role_id\" parameter could not be converted to int") + } else if defaultRole, err := r.GetRole(ctx, int32(defaultRoleIdInt)); err != nil { + return nil, fmt.Errorf("\"config.auto_provision.default_role_id\" parameter is invalid") + } else if roleProvision, hasRoleProvisioned := multipartForm.Value["config.auto_provision.role_provision"]; !hasRoleProvisioned { + return nil, fmt.Errorf("form is missing \"config.auto_provision.role_provision\" parameter") + } else if len(roleProvision) > 1 { + return nil, fmt.Errorf("\"config.auto_provision.role_provision\" has more than one value") + } else if isRoleProvisioned, err := strconv.ParseBool(roleProvision[0]); err != nil { + return nil, fmt.Errorf("\"config.auto_provision.role_provision\" parameter could not be converted to bool") + } else { + return &model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: isAutoProvisionEnabled, + DefaultRoleId: defaultRole.ID, + RoleProvision: isRoleProvisioned, + }, + }, nil + } +} + // This retains support for the old saml login urls /api/{version}/login/saml/ that were added to their respective IDPs func (s ManagementResource) SAMLLoginRedirect(response http.ResponseWriter, request *http.Request) { ssoProviderSlug := mux.Vars(request)[api.URIPathVariableSSOProviderSlug] @@ -126,46 +212,28 @@ func (s ManagementResource) GetSAMLProvider(response http.ResponseWriter, reques } func (s ManagementResource) CreateSAMLProviderMultipart(response http.ResponseWriter, request *http.Request) { - var samlIdentityProvider model.SAMLProvider - if err := request.ParseMultipartForm(api.DefaultAPIPayloadReadLimitBytes); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if providerNames, hasProviderName := request.MultipartForm.Value["name"]; !hasProviderName { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "form is missing \"name\" parameter", request), response) - } else if numProviderNames := len(providerNames); numProviderNames == 0 || numProviderNames > 1 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "expected only one \"name\" parameter", request), response) - } else if metadataXMLFileHandles, hasMetadataXML := request.MultipartForm.File["metadata"]; !hasMetadataXML { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "form is missing \"metadata\" parameter", request), response) - } else if numHeaders := len(metadataXMLFileHandles); numHeaders == 0 || numHeaders > 1 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "expected only one \"metadata\" parameter", request), response) - } else if metadataXMLReader, err := metadataXMLFileHandles[0].Open(); err != nil { + } else if providerName, err := getProviderNameFromMultipartRequest(request.MultipartForm, true); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if metadataXML, metadata, err := getMetadataFromMultipartRequest(request.MultipartForm, true); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if config, err := getSSOProviderConfigFromMultipartRequest(request.Context(), request.MultipartForm, true, s.db); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if ssoURL, err := auth.GetIDPSingleSignOnServiceURL(metadata, saml.HTTPPostBinding); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "metadata does not have a SSO service that supports HTTP POST binding", request), response) + } else if newSAMLProvider, err := s.db.CreateSAMLIdentityProvider(request.Context(), model.SAMLProvider{ + Name: providerName, + DisplayName: providerName, + MetadataXML: metadataXML, + IssuerURI: metadata.EntityID, + SingleSignOnURI: ssoURL, + }, *config); errors.Is(err, database.ErrDuplicateSSOProviderName) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) + } else if err != nil { + api.HandleDatabaseError(request, response, err) } else { - defer metadataXMLReader.Close() - - if metadataXML, err := io.ReadAll(metadataXMLReader); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if metadata, err := samlsp.ParseMetadata(metadataXML); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if ssoURL, err := auth.GetIDPSingleSignOnServiceURL(metadata, saml.HTTPPostBinding); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "metadata does not have a SSO service that supports HTTP POST binding", request), response) - } else { - samlIdentityProvider.Name = providerNames[0] - samlIdentityProvider.DisplayName = providerNames[0] - samlIdentityProvider.MetadataXML = metadataXML - samlIdentityProvider.IssuerURI = metadata.EntityID - samlIdentityProvider.SingleSignOnURI = ssoURL - - if newSAMLProvider, err := s.db.CreateSAMLIdentityProvider(request.Context(), samlIdentityProvider); err != nil { - if errors.Is(err, database.ErrDuplicateSSOProviderName) { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) - } else { - api.HandleDatabaseError(request, response, err) - } - } else { - api.WriteBasicResponse(request.Context(), newSAMLProvider, http.StatusOK, response) - } - } + api.WriteBasicResponse(request.Context(), newSAMLProvider, http.StatusOK, response) } } @@ -199,70 +267,69 @@ func (s ManagementResource) UpdateSAMLProviderRequest(response http.ResponseWrit api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusNotFound, api.ErrorResponseDetailsResourceNotFound, request), response) } else if err := request.ParseMultipartForm(api.DefaultAPIPayloadReadLimitBytes); err != nil { api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - } else if providerNames, hasProviderName := request.MultipartForm.Value["name"]; len(providerNames) > 1 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "expected only one \"name\" parameter", request), response) - } else if metadataXMLFileHandles, hasMetadataXML := request.MultipartForm.File["metadata"]; len(metadataXMLFileHandles) > 1 { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "expected only one \"metadata\" parameter", request), response) + } else if providerName, err := getProviderNameFromMultipartRequest(request.MultipartForm, false); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if metadataXML, metadata, err := getMetadataFromMultipartRequest(request.MultipartForm, false); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if config, err := getSSOProviderConfigFromMultipartRequest(request.Context(), request.MultipartForm, false, s.db); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if ssoProvider, err := updateSAMLProvider(ssoProvider, providerName, metadataXML, metadata, config); err != nil { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) + } else if newSAMLProvider, err := s.db.UpdateSAMLIdentityProvider(request.Context(), ssoProvider); errors.Is(err, database.ErrDuplicateSSOProviderName) { + api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) + } else if err != nil { + api.HandleDatabaseError(request, response, err) } else { - if hasProviderName { - ssoProvider.Name = providerNames[0] + api.WriteBasicResponse(request.Context(), newSAMLProvider, http.StatusOK, response) + } +} + +// updateSAMLProvider Assumes role id has been validated already +func updateSAMLProvider(ssoProvider model.SSOProvider, providerName string, metadataXML []byte, metadata *saml.EntityDescriptor, config *model.SSOProviderConfig) (model.SSOProvider, error) { + if ssoProvider.SAMLProvider == nil { + return ssoProvider, ErrSAMLProviderMissing + } - ssoProvider.SAMLProvider.Name = providerNames[0] - ssoProvider.SAMLProvider.DisplayName = providerNames[0] + if providerName != "" { + ssoProvider.Name = providerName + + ssoProvider.SAMLProvider.Name = providerName + ssoProvider.SAMLProvider.DisplayName = providerName + } + + if metadataXML != nil { + if ssoURL, err := auth.GetIDPSingleSignOnServiceURL(metadata, saml.HTTPPostBinding); err != nil { + return ssoProvider, fmt.Errorf("metadata does not have a SSO service that supports HTTP POST binding") + } else { + ssoProvider.SAMLProvider.MetadataXML = metadataXML + ssoProvider.SAMLProvider.IssuerURI = metadata.EntityID + ssoProvider.SAMLProvider.SingleSignOnURI = ssoURL } - if hasMetadataXML { - if metadataXMLReader, err := metadataXMLFileHandles[0].Open(); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - return - } else { - defer metadataXMLReader.Close() - - if metadataXML, err := io.ReadAll(metadataXMLReader); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - return - } else if metadata, err := samlsp.ParseMetadata(metadataXML); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, err.Error(), request), response) - return - } else if ssoURL, err := auth.GetIDPSingleSignOnServiceURL(metadata, saml.HTTPPostBinding); err != nil { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "metadata does not have a SSO service that supports HTTP POST binding", request), response) - return - } else { - ssoProvider.SAMLProvider.MetadataXML = metadataXML - ssoProvider.SAMLProvider.IssuerURI = metadata.EntityID - ssoProvider.SAMLProvider.SingleSignOnURI = ssoURL - - // It's possible to update the ACS url which will be reflected in the metadataXML, we need to guarantee it is set to only what we expect if it is present - if acsUrl, err := auth.GetAssertionConsumerServiceURL(metadata, saml.HTTPPostBinding); err == nil { - if !strings.Contains(acsUrl, model.SAMLRootURIVersionMap[ssoProvider.SAMLProvider.RootURIVersion]) { - var validUri bool - for rootUriVersion, path := range model.SAMLRootURIVersionMap { - if strings.Contains(acsUrl, path) { - ssoProvider.SAMLProvider.RootURIVersion = rootUriVersion - validUri = true - break - } - } - if !validUri { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusBadRequest, "metadata does not have a valid ACS location", request), response) - return - } - } + // It's possible to update the ACS url which will be reflected in the metadataXML, we need to guarantee it is set to only what we expect if it is present + if acsUrl, err := auth.GetAssertionConsumerServiceURL(metadata, saml.HTTPPostBinding); err == nil { + if !strings.Contains(acsUrl, model.SAMLRootURIVersionMap[ssoProvider.SAMLProvider.RootURIVersion]) { + var validUri bool + for rootUriVersion, path := range model.SAMLRootURIVersionMap { + if strings.Contains(acsUrl, path) { + ssoProvider.SAMLProvider.RootURIVersion = rootUriVersion + validUri = true + break } } + if !validUri { + return ssoProvider, fmt.Errorf("metadata does not have a valid ACS location") + } } } + } - if newSAMLProvider, err := s.db.UpdateSAMLIdentityProvider(request.Context(), ssoProvider); err != nil { - if errors.Is(err, database.ErrDuplicateSSOProviderName) { - api.WriteErrorResponse(request.Context(), api.BuildErrorResponse(http.StatusConflict, api.ErrorResponseSSOProviderDuplicateName, request), response) - } else { - api.HandleDatabaseError(request, response, err) - } - } else { - api.WriteBasicResponse(request.Context(), newSAMLProvider, http.StatusOK, response) - } + // Need to ensure that if no config is specified, we don't accidentally wipe the existing configuration + if config != nil { + ssoProvider.Config = *config } + + return ssoProvider, nil } // Preserve old metadata endpoint for saml providers @@ -374,30 +441,65 @@ func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, re } else if serviceProvider, err := auth.NewServiceProvider(*ctx.Get(request.Context()).Host, s.config, *ssoProvider.SAMLProvider); err != nil { log.Errorf("[SAML] Service provider creation failed: %v", err) redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + } else if err := request.ParseForm(); err != nil { + log.Errorf("[SAML] Failed to parse form POST: %v", err) + // Technical issues or invalid form data + // This is not covered by acceptance criteria directly; treat as technical issue + redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + } else if assertion, err := serviceProvider.ParseResponse(request, nil); err != nil { + var typedErr *saml.InvalidResponseError + switch { + case errors.As(err, &typedErr): + log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v - %s", ssoProvider.SAMLProvider.IssuerURI, typedErr.PrivateErr, typedErr.Response) + default: + log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v", ssoProvider.SAMLProvider.IssuerURI, err) + } + // SAML credentials issue scenario (authentication failed) + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") + } else if principalName, err := ssoProvider.SAMLProvider.GetSAMLUserPrincipalNameFromAssertion(assertion); err != nil { + log.Errorf("[SAML] Failed to lookup user for SAML provider %s: %v", ssoProvider.Name, err) + // SAML credentials issue scenario again + redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { - if err := request.ParseForm(); err != nil { - log.Errorf("[SAML] Failed to parse form POST: %v", err) - // Technical issues or invalid form data - // This is not covered by acceptance criteria directly; treat as technical issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") - } else { - if assertion, err := serviceProvider.ParseResponse(request, nil); err != nil { - var typedErr *saml.InvalidResponseError - switch { - case errors.As(err, &typedErr): - log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v - %s", ssoProvider.SAMLProvider.IssuerURI, typedErr.PrivateErr, typedErr.Response) - default: - log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v", ssoProvider.SAMLProvider.IssuerURI, err) - } - // SAML credentials issue scenario (authentication failed) - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") - } else if principalName, err := ssoProvider.SAMLProvider.GetSAMLUserPrincipalNameFromAssertion(assertion); err != nil { - log.Errorf("[SAML] Failed to lookup user for SAML provider %s: %v", ssoProvider.Name, err) - // SAML credentials issue scenario again - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") - } else { - s.authenticator.CreateSSOSession(request, response, principalName, ssoProvider) + if ssoProvider.Config.AutoProvision.Enabled { + if err := jitSAMLUserCreation(request.Context(), ssoProvider, principalName, assertion, s.db); err != nil { + // It is safe to let this request drop into the CreateSSOSession function below to ensure proper audit logging + log.Errorf("[SAML] Error during JIT User Creation: %v", err) } } + + s.authenticator.CreateSSOSession(request, response, principalName, ssoProvider) + } +} + +func jitSAMLUserCreation(ctx context.Context, ssoProvider model.SSOProvider, principalName string, assertion *saml.Assertion, u jitUserCreator) error { + if role, err := u.GetRole(ctx, ssoProvider.Config.AutoProvision.DefaultRoleId); err != nil { + return fmt.Errorf("get role: %v", err) + } else if _, err := u.LookupUser(ctx, principalName); err != nil && !errors.Is(err, database.ErrNotFound) { + return fmt.Errorf("lookup user: %v", err) + } else if errors.Is(err, database.ErrNotFound) { + user := model.User{ + EmailAddress: null.StringFrom(principalName), + PrincipalName: principalName, + Roles: model.Roles{role}, + SSOProviderID: null.Int32From(ssoProvider.ID), + EULAAccepted: true, // EULA Acceptance does not pertain to Bloodhound Community Edition; this flag is used for Bloodhound Enterprise users + FirstName: null.StringFrom(principalName), + LastName: null.StringFrom("Last name not found"), + } + + if givenName, err := ssoProvider.SAMLProvider.GetSAMLUserGivenNameFromAssertion(assertion); err == nil { + user.FirstName = null.StringFrom(givenName) + } + + if surname, err := ssoProvider.SAMLProvider.GetSAMLUserSurnameFromAssertion(assertion); err == nil { + user.LastName = null.StringFrom(surname) + } + + if _, err := u.CreateUser(ctx, user); err != nil { + return fmt.Errorf("create user: %v", err) + } } + + return nil } diff --git a/cmd/api/src/api/v2/auth/sso.go b/cmd/api/src/api/v2/auth/sso.go index ead4542865..27d49aa6fe 100644 --- a/cmd/api/src/api/v2/auth/sso.go +++ b/cmd/api/src/api/v2/auth/sso.go @@ -17,6 +17,7 @@ package auth import ( + "context" "net/http" "net/url" "path" @@ -37,11 +38,12 @@ import ( // AuthProvider represents a unified SSO provider (either OIDC or SAML) type AuthProvider struct { - ID int32 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Slug string `json:"slug"` - Details interface{} `json:"details"` + ID int32 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Slug string `json:"slug"` + Details interface{} `json:"details"` + Config model.SSOProviderConfig `json:"config"` LoginUri serde.URL `json:"login_uri"` CallbackUri serde.URL `json:"callback_uri"` @@ -55,6 +57,17 @@ func (s *AuthProvider) FormatProviderURLs(hostUrl url.URL) { s.CallbackUri = serde.FromURL(*root.JoinPath("callback")) } +type getRoler interface { + GetRole(ctx context.Context, roleID int32) (model.Role, error) +} + +type jitUserCreator interface { + getRoler + + LookupUser(ctx context.Context, principalNameOrEmail string) (model.User, error) + CreateUser(ctx context.Context, user model.User) (model.User, error) +} + // ListAuthProviders lists all available SSO providers (SAML and OIDC) with sorting and filtering func (s ManagementResource) ListAuthProviders(response http.ResponseWriter, request *http.Request) { var ( @@ -119,10 +132,11 @@ func (s ManagementResource) ListAuthProviders(response http.ResponseWriter, requ } else { for _, ssoProvider := range ssoProviders { provider := AuthProvider{ - ID: ssoProvider.ID, - Name: ssoProvider.Name, - Type: ssoProvider.Type.String(), - Slug: ssoProvider.Slug, + ID: ssoProvider.ID, + Name: ssoProvider.Name, + Type: ssoProvider.Type.String(), + Slug: ssoProvider.Slug, + Config: ssoProvider.Config, } // Format callback url from host diff --git a/cmd/api/src/api/v2/auth/sso_test.go b/cmd/api/src/api/v2/auth/sso_test.go index 9ab0592bfa..7d97ade078 100644 --- a/cmd/api/src/api/v2/auth/sso_test.go +++ b/cmd/api/src/api/v2/auth/sso_test.go @@ -17,13 +17,10 @@ package auth_test import ( - "context" "net/http" - "net/http/httptest" "net/url" "testing" - "github.com/gorilla/mux" "github.com/pkg/errors" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/api/v2/apitest" @@ -34,37 +31,43 @@ import ( "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/utils/test" - "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) func TestManagementResource_ListAuthProviders(t *testing.T) { - mockCtrl := gomock.NewController(t) - resources, mockDB := apitest.NewAuthManagementResource(mockCtrl) - defer mockCtrl.Finish() + const endpoint = "/api/v2/sso-providers" - t.Run("successfully list auth providers without query parameters", func(t *testing.T) { - oidcProvider := model.OIDCProvider{ + var ( + mockCtrl = gomock.NewController(t) + resources, mockDB = apitest.NewAuthManagementResource(mockCtrl) + reqCtx = &ctx.Context{Host: &url.URL{}} + + oidcProvider = model.OIDCProvider{ SSOProviderID: 1, ClientID: "client-id-1", Issuer: "https://issuer1.com", } - - samlProvider := model.SAMLProvider{ + samlProvider = model.SAMLProvider{ Serial: model.Serial{ID: 2}, Name: "SAML Provider 1", DisplayName: "SAML Provider One", IssuerURI: "https://saml-issuer1.com", SSOProviderID: null.Int32From(2), } - - ssoProviders := []model.SSOProvider{ + ssoProviders = []model.SSOProvider{ { Serial: model.Serial{ID: 1}, Name: "OIDC Provider 1", Slug: "oidc-provider-1", Type: model.SessionAuthProviderOIDC, OIDCProvider: &oidcProvider, + Config: model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + }, }, { Serial: model.Serial{ID: 2}, @@ -72,202 +75,84 @@ func TestManagementResource_ListAuthProviders(t *testing.T) { Slug: "saml-provider-1", Type: model.SessionAuthProviderSAML, SAMLProvider: &samlProvider, + Config: model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 2, + RoleProvision: false, + }, + }, }, } + ) + defer mockCtrl.Finish() + t.Run("successfully list auth providers without query parameters", func(t *testing.T) { // default ordering and no filters - mockDB.EXPECT().GetAllSSOProviders( - gomock.Any(), - "created_at", - model.SQLFilter{SQLString: "", Params: nil}, - ).Return(ssoProviders, nil) - - endpoint := "/api/v2/sso-providers" - - bhCtx := &ctx.Context{ - Host: &url.URL{ - Scheme: "http", - Host: "example.com", - }, - } - requestContext := context.WithValue(context.Background(), ctx.ValueKey, bhCtx) + mockDB.EXPECT().GetAllSSOProviders(gomock.Any(), "created_at", model.SQLFilter{}).Return(ssoProviders, nil) - req, err := http.NewRequestWithContext(requestContext, "GET", endpoint, nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Host = "example.com" - - router := mux.NewRouter() - router.HandleFunc(endpoint, resources.ListAuthProviders).Methods("GET") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusOK, rr.Code) + test.Request(t). + WithMethod(http.MethodGet). + WithContext(reqCtx). + WithURL(endpoint). + OnHandlerFunc(resources.ListAuthProviders). + Require(). + ResponseStatusCode(http.StatusOK) }) - oidcProvider := model.OIDCProvider{ - SSOProviderID: 1, - ClientID: "client-id-1", - Issuer: "https://issuer1.com", - } - - samlProvider := model.SAMLProvider{ - Serial: model.Serial{ID: 2}, - Name: "SAML Provider 1", - DisplayName: "SAML Provider One", - IssuerURI: "https://saml-issuer1.com", - SSOProviderID: null.Int32From(2), - } - - ssoProviders := []model.SSOProvider{ - { - Serial: model.Serial{ID: 2}, - Name: "SAML Provider 1", - Slug: "saml-provider-1", - Type: model.SessionAuthProviderSAML, - SAMLProvider: &samlProvider, - }, - { - Serial: model.Serial{ID: 1}, - Name: "OIDC Provider 1", - Slug: "oidc-provider-1", - Type: model.SessionAuthProviderOIDC, - OIDCProvider: &oidcProvider, - }, - } - t.Run("successfully list auth providers with sorting", func(t *testing.T) { - // sorting by name descending - mockDB.EXPECT().GetAllSSOProviders( - gomock.Any(), - "name desc", - model.SQLFilter{SQLString: "", Params: nil}, - ).Return(ssoProviders, nil) - - endpoint := "/api/v2/sso-providers?sort_by=-name" - - bhCtx := &ctx.Context{ - Host: &url.URL{ - Scheme: "http", - Host: "example.com", - }, - } - requestContext := context.WithValue(context.Background(), ctx.ValueKey, bhCtx) + mockDB.EXPECT().GetAllSSOProviders(gomock.Any(), "name desc", model.SQLFilter{SQLString: "", Params: nil}).Return(ssoProviders, nil) + const reqUrl = endpoint + "?sort_by=-name" - req, err := http.NewRequestWithContext(requestContext, "GET", endpoint, nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Host = "example.com" - - router := mux.NewRouter() - router.HandleFunc("/api/v2/sso-providers", resources.ListAuthProviders).Methods("GET") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusOK, rr.Code) + test.Request(t). + WithMethod(http.MethodGet). + WithContext(reqCtx). + WithURL(reqUrl). + OnHandlerFunc(resources.ListAuthProviders). + Require(). + ResponseStatusCode(http.StatusOK) }) t.Run("successfully list auth providers with filtering", func(t *testing.T) { - oidcProvider := model.OIDCProvider{ - SSOProviderID: 1, - ClientID: "client-id-1", - Issuer: "https://issuer1.com", - } - ssoProviders := []model.SSOProvider{ - { - Serial: model.Serial{ID: 1}, - Name: "OIDC Provider 1", - Slug: "oidc-provider-1", - Type: model.SessionAuthProviderOIDC, - OIDCProvider: &oidcProvider, - }, - } - // filtering by name - mockDB.EXPECT().GetAllSSOProviders( - gomock.Any(), - "created_at", - model.SQLFilter{ - SQLString: "name = ?", - Params: []interface{}{"OIDC Provider 1"}, - }, - ).Return(ssoProviders, nil) + mockDB.EXPECT().GetAllSSOProviders(gomock.Any(), "created_at", model.SQLFilter{ + SQLString: "name = ?", + Params: []interface{}{"OIDC Provider 1"}, + }).Return([]model.SSOProvider{ssoProviders[0]}, nil) + const reqUrl = endpoint + "?name=eq:OIDC Provider 1" - endpoint := "/api/v2/sso-providers?name=eq:OIDC Provider 1" - - bhCtx := &ctx.Context{ - Host: &url.URL{ - Scheme: "http", - Host: "example.com", - }, - } - requestContext := context.WithValue(context.Background(), ctx.ValueKey, bhCtx) - - req, err := http.NewRequestWithContext(requestContext, "GET", endpoint, nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Host = "example.com" - - router := mux.NewRouter() - router.HandleFunc("/api/v2/sso-providers", resources.ListAuthProviders).Methods("GET") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusOK, rr.Code) + test.Request(t). + WithMethod(http.MethodGet). + WithContext(reqCtx). + WithURL(reqUrl). + OnHandlerFunc(resources.ListAuthProviders). + Require(). + ResponseStatusCode(http.StatusOK) }) t.Run("fail to list auth providers with invalid sort field", func(t *testing.T) { - endpoint := "/api/v2/sso-providers?sort_by=invalid_field" + const reqUrl = endpoint + "?sort_by=invalid_field" - bhCtx := &ctx.Context{ - Host: &url.URL{ - Scheme: "http", - Host: "example.com", - }, - } - requestContext := context.WithValue(context.Background(), ctx.ValueKey, bhCtx) - - req, err := http.NewRequestWithContext(requestContext, "GET", endpoint, nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Host = "example.com" - - router := mux.NewRouter() - router.HandleFunc("/api/v2/sso-providers", resources.ListAuthProviders).Methods("GET") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusBadRequest, rr.Code) + test.Request(t). + WithMethod(http.MethodGet). + WithContext(reqCtx). + WithURL(reqUrl). + OnHandlerFunc(resources.ListAuthProviders). + Require(). + ResponseStatusCode(http.StatusBadRequest) }) t.Run("fail to list auth providers with invalid filter predicate", func(t *testing.T) { - endpoint := "/api/v2/sso-providers?name=invalid_predicate:Provider" + const reqUrl = endpoint + "?name=invalid_predicate:Provider" - bhCtx := &ctx.Context{ - Host: &url.URL{ - Scheme: "http", - Host: "example.com", - }, - } - requestContext := context.WithValue(context.Background(), ctx.ValueKey, bhCtx) - - req, err := http.NewRequestWithContext(requestContext, "GET", endpoint, nil) - require.NoError(t, err) - req.Header.Set("Content-Type", "application/json") - req.Host = "example.com" - - router := mux.NewRouter() - router.HandleFunc("/api/v2/sso-providers", resources.ListAuthProviders).Methods("GET") - - rr := httptest.NewRecorder() - router.ServeHTTP(rr, req) - - require.Equal(t, http.StatusBadRequest, rr.Code) + test.Request(t). + WithMethod(http.MethodGet). + WithContext(reqCtx). + WithURL(reqUrl). + OnHandlerFunc(resources.ListAuthProviders). + Require(). + ResponseStatusCode(http.StatusBadRequest) }) } diff --git a/cmd/api/src/database/auth_test.go b/cmd/api/src/database/auth_test.go index 1170bd3b7b..4da18dee24 100644 --- a/cmd/api/src/database/auth_test.go +++ b/cmd/api/src/database/auth_test.go @@ -37,7 +37,10 @@ import ( const ( userPrincipal = "first.last@example.com" user2Principal = "first2.last2@example.com" - roleToDelete = auth.RoleReadOnly + user3Principal = "first3.last3@example.com" + user4Principal = "first4.last4@example.com" + + roleToDelete = auth.RoleReadOnly ) func initAndGetRoles(t *testing.T) (database.Database, model.Roles) { @@ -236,7 +239,7 @@ func TestDatabase_UpdateUserAuth(t *testing.T) { } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionCreateAuthSecret, "secret_user_id", newSecret.UserID.String()); err != nil { t.Fatalf("Failed to validate CreateAuthSecret audit logs:\n%v", err) } else { - if newSAMLProvider, err := dbInst.CreateSAMLIdentityProvider(ctx, samlProvider); err != nil { + if newSAMLProvider, err := dbInst.CreateSAMLIdentityProvider(ctx, samlProvider, model.SSOProviderConfig{}); err != nil { t.Fatalf("Failed to create SAML provider: %v", err) } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionCreateSAMLIdentityProvider, "saml_name", newSAMLProvider.Name); err != nil { t.Fatalf("Failed to validate CreateSAMLIdentityProvider audit logs:\n%v", err) @@ -363,71 +366,145 @@ func TestDatabase_CreateGetDeleteAuthSecret(t *testing.T) { } } -func TestDatabase_CreateUpdateDeleteSAMLProvider(t *testing.T) { - var ( - ctx = context.Background() - dbInst, user = initAndCreateUser(t) - samlProvider model.SAMLProvider - newSAMLProvider model.SAMLProvider - updatedUser model.User - err error - ) - // Initialize the SAMLProvider without setting SSOProviderID - samlProvider = model.SAMLProvider{ - Name: "provider", - DisplayName: "provider name", - IssuerURI: "https://idp.example.com/idp.xml", - SingleSignOnURI: "https://idp.example.com/sso", - } +func TestDatabase_CreateUpdateDeleteSSOProvider(t *testing.T) { + t.Run("successfully CreateUpdateDeleteSAMLProvider", func(t *testing.T) { + var ( + ctx = context.Background() + dbInst, user = initAndCreateUser(t) + samlProvider model.SAMLProvider + newSAMLProvider model.SAMLProvider + updatedUser model.User + config = model.SSOProviderConfig{} + err error + ) + // Initialize the SAMLProvider without setting SSOProviderID + samlProvider = model.SAMLProvider{ + Name: "provider", + DisplayName: "provider name", + IssuerURI: "https://idp.example.com/idp.xml", + SingleSignOnURI: "https://idp.example.com/sso", + } - if newSAMLProvider, err = dbInst.CreateSAMLIdentityProvider(ctx, samlProvider); err != nil { - t.Fatalf("Failed to create SAML provider: %v", err) - } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionCreateSAMLIdentityProvider, "saml_name", newSAMLProvider.Name); err != nil { - t.Fatalf("Failed to validate CreateSAMLIdentityProvider audit logs:\n%v", err) - } else { - user.SSOProviderID = newSAMLProvider.SSOProviderID - if err = dbInst.UpdateUser(ctx, user); err != nil { - t.Fatalf("Failed to update user: %v", err) - } else if updatedUser, err = dbInst.GetUser(ctx, user.ID); err != nil { - t.Fatalf("Failed to fetch updated user: %v", err) - } else if updatedUser.SSOProvider == nil { - t.Fatalf("Updated user does not have a SAMLProvider set when it should") - } else if updatedUser.SSOProvider.ID != newSAMLProvider.SSOProviderID.Int32 { - t.Fatalf("Updated user has SSOProvider ID %d when %v was expected", updatedUser.SSOProvider.ID, newSAMLProvider.SSOProviderID) - } else if updatedUser.SSOProvider.SAMLProvider.IssuerURI != newSAMLProvider.IssuerURI { - t.Fatalf("Updated user has SAMLProvider URL %s when %s was expected", updatedUser.SSOProvider.SAMLProvider.IssuerURI, newSAMLProvider.IssuerURI) + if newSAMLProvider, err = dbInst.CreateSAMLIdentityProvider(ctx, samlProvider, config); err != nil { + t.Fatalf("Failed to create SAML provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionCreateSAMLIdentityProvider, "saml_name", newSAMLProvider.Name); err != nil { + t.Fatalf("Failed to validate CreateSAMLIdentityProvider audit logs:\n%v", err) } else { - updatedSSOProvider := model.SSOProvider{ - Name: "updated provider", - Type: model.SessionAuthProviderSAML, - SAMLProvider: &model.SAMLProvider{ - Serial: model.Serial{ - ID: newSAMLProvider.ID, + user.SSOProviderID = newSAMLProvider.SSOProviderID + if err = dbInst.UpdateUser(ctx, user); err != nil { + t.Fatalf("Failed to update user: %v", err) + } else if updatedUser, err = dbInst.GetUser(ctx, user.ID); err != nil { + t.Fatalf("Failed to fetch updated user: %v", err) + } else if updatedUser.SSOProvider == nil { + t.Fatalf("Updated user does not have a SAMLProvider set when it should") + } else if updatedUser.SSOProvider.ID != newSAMLProvider.SSOProviderID.Int32 { + t.Fatalf("Updated user has SSOProvider ID %d when %v was expected", updatedUser.SSOProvider.ID, newSAMLProvider.SSOProviderID) + } else if updatedUser.SSOProvider.SAMLProvider.IssuerURI != newSAMLProvider.IssuerURI { + t.Fatalf("Updated user has SAMLProvider URL %s when %s was expected", updatedUser.SSOProvider.SAMLProvider.IssuerURI, newSAMLProvider.IssuerURI) + } else { + updatedSSOProvider := model.SSOProvider{ + Name: "updated provider", + Type: model.SessionAuthProviderSAML, + SAMLProvider: &model.SAMLProvider{ + Serial: model.Serial{ + ID: newSAMLProvider.ID, + }, + Name: "updated provider", + DisplayName: newSAMLProvider.DisplayName, + IssuerURI: newSAMLProvider.IssuerURI, + SingleSignOnURI: newSAMLProvider.SingleSignOnURI, + SSOProviderID: newSAMLProvider.SSOProviderID, }, - Name: "updated provider", - DisplayName: newSAMLProvider.DisplayName, - IssuerURI: newSAMLProvider.IssuerURI, - SingleSignOnURI: newSAMLProvider.SingleSignOnURI, - SSOProviderID: newSAMLProvider.SSOProviderID, + Config: config, + } + + if _, err = dbInst.UpdateSAMLIdentityProvider(ctx, updatedSSOProvider); err != nil { + t.Fatalf("Failed to update SAML provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionUpdateSAMLIdentityProvider, "saml_name", "updated provider"); err != nil { + t.Fatalf("Failed to validate UpdateSAMLIdentityProvider audit logs:\n%v", err) + } else { + user.SSOProviderID = null.Int32{} + if err = dbInst.UpdateUser(ctx, user); err != nil { + t.Fatalf("Failed to update user: %v", err) + } else if err = dbInst.DeleteSSOProvider(ctx, int(newSAMLProvider.SSOProviderID.Int32)); err != nil { + t.Fatalf("Failed to delete SAML provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionDeleteSSOIdentityProvider, "name", "provider"); err != nil { + t.Fatalf("Failed to validate DeleteSAMLIdentityProvider audit logs:\n%v", err) + } + } + } + } + }) + + t.Run("successfully CreateUpdateDeleteOIDCProvider", func(t *testing.T) { + var ( + testCtx = context.Background() + dbInst, user = initAndCreateUser(t) + oidcProvider = model.OIDCProvider{ + ClientID: "bloodhound", + Issuer: "https://localhost/auth", + } + updatedUser model.User + emptyConfig = model.SSOProviderConfig{} + config = model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, }, } + ) - if _, err = dbInst.UpdateSAMLIdentityProvider(ctx, updatedSSOProvider); err != nil { - t.Fatalf("Failed to update SAML provider: %v", err) - } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionUpdateSAMLIdentityProvider, "saml_name", "updated provider"); err != nil { - t.Fatalf("Failed to validate UpdateSAMLIdentityProvider audit logs:\n%v", err) + if newOIDCProvider, err := dbInst.CreateOIDCProvider(testCtx, "test_oidc", oidcProvider.Issuer, oidcProvider.ClientID, emptyConfig); err != nil { + t.Fatalf("Failed to create OIDC provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionCreateOIDCIdentityProvider, "client_id", "bloodhound"); err != nil { + t.Fatalf("Failed to validate CreateOIDCIdentityProvider audit logs:\n%v", err) + } else { + user.SSOProviderID = null.Int32From(int32(newOIDCProvider.SSOProviderID)) + if err = dbInst.UpdateUser(testCtx, user); err != nil { + t.Fatalf("Failed to update user: %v", err) + } else if updatedUser, err = dbInst.GetUser(testCtx, user.ID); err != nil { + t.Fatalf("Failed to fetch updated user: %v", err) + } else if updatedUser.SSOProvider == nil { + t.Fatalf("Updated user does not have a OIDCProvider set when it should") + } else if updatedUser.SSOProvider.ID != int32(newOIDCProvider.SSOProviderID) { + t.Fatalf("Updated user has SSOProvider ID %d when %v was expected", updatedUser.SSOProvider.ID, newOIDCProvider.ID) + } else if updatedUser.SSOProvider.OIDCProvider.Issuer != newOIDCProvider.Issuer { + t.Fatalf("Updated user has OIDCProvider Issuer %s when %s was expected", updatedUser.SSOProvider.OIDCProvider.Issuer, newOIDCProvider.Issuer) + } else if updatedUser.SSOProvider.Config != emptyConfig { + t.Fatalf("Updated user has Config %v when %v was expected", updatedUser.SSOProvider.Config, emptyConfig) } else { - user.SSOProviderID = null.Int32{} - if err = dbInst.UpdateUser(ctx, user); err != nil { - t.Fatalf("Failed to update user: %v", err) - } else if err = dbInst.DeleteSSOProvider(ctx, int(newSAMLProvider.SSOProviderID.Int32)); err != nil { - t.Fatalf("Failed to delete SAML provider: %v", err) - } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionDeleteSSOIdentityProvider, "name", "provider"); err != nil { - t.Fatalf("Failed to validate DeleteSAMLIdentityProvider audit logs:\n%v", err) + updatedSSOProvider := model.SSOProvider{ + Name: "updated provider", + Type: model.SessionAuthProviderOIDC, + OIDCProvider: &model.OIDCProvider{ + Serial: model.Serial{ + ID: newOIDCProvider.ID, + }, + Issuer: newOIDCProvider.Issuer, + ClientID: newOIDCProvider.ClientID, + SSOProviderID: newOIDCProvider.SSOProviderID, + }, + Config: config, + } + + if _, err = dbInst.UpdateOIDCProvider(testCtx, updatedSSOProvider); err != nil { + t.Fatalf("Failed to update OIDC provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionUpdateOIDCIdentityProvider, "client_id", "bloodhound"); err != nil { + t.Fatalf("Failed to validate UpdateOIDCIdentityProvider audit logs:\n%v", err) + } else { + user.SSOProviderID = null.Int32{} + if err = dbInst.UpdateUser(testCtx, user); err != nil { + t.Fatalf("Failed to update user: %v", err) + } else if err = dbInst.DeleteSSOProvider(testCtx, int(newOIDCProvider.SSOProviderID)); err != nil { + t.Fatalf("Failed to delete OIDC provider: %v", err) + } else if err = test.VerifyAuditLogs(dbInst, model.AuditLogActionDeleteSSOIdentityProvider, "name", "test_oidc"); err != nil { + t.Fatalf("Failed to validate DeleteSSOIdentityProvider audit logs:\n%v", err) + } } } } - } + }) } func TestDatabase_CreateUserSession(t *testing.T) { @@ -484,38 +561,75 @@ func TestDatabase_SetUserSessionFlag(t *testing.T) { } func TestDatabase_GetUserSSOSession(t *testing.T) { - var ( - testCtx = context.Background() - dbInst, user = initAndCreateUser(t) - samlProvider = model.SAMLProvider{ - Name: "provider", - DisplayName: "provider name", - IssuerURI: "https://idp.example.com/idp.xml", - SingleSignOnURI: "https://idp.example.com/sso", - } - ) + t.Run("Successful GetUserSSOSession (SAML)", func(t *testing.T) { + var ( + testCtx = context.Background() + dbInst, user = initAndCreateUser(t) + samlProvider = model.SAMLProvider{ + Name: "provider", + DisplayName: "provider name", + IssuerURI: "https://idp.example.com/idp.xml", + SingleSignOnURI: "https://idp.example.com/sso", + } + ) - // Initialize the SAMLProvider without setting SSOProviderID - newSAMLProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, samlProvider) - require.Nil(t, err) + // Initialize the SAMLProvider without setting SSOProviderID + newSAMLProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, samlProvider, model.SSOProviderConfig{}) + require.Nil(t, err) - user.SSOProviderID = newSAMLProvider.SSOProviderID - err = dbInst.UpdateUser(testCtx, user) - require.Nil(t, err) + user.SSOProviderID = newSAMLProvider.SSOProviderID + err = dbInst.UpdateUser(testCtx, user) + require.Nil(t, err) + + userSession := model.UserSession{ + AuthProviderID: newSAMLProvider.ID, + AuthProviderType: model.SessionAuthProviderSAML, + User: user, + UserID: user.ID, + ExpiresAt: time.Now().UTC().Add(time.Hour), + } - userSession := model.UserSession{ - AuthProviderID: newSAMLProvider.ID, - AuthProviderType: model.SessionAuthProviderSAML, - User: user, - UserID: user.ID, - ExpiresAt: time.Now().UTC().Add(time.Hour), - } + newUserSession, err := dbInst.CreateUserSession(testCtx, userSession) + require.Nil(t, err) + + dbSess, err := dbInst.GetUserSession(testCtx, newUserSession.ID) + require.Nil(t, err) + require.NotNil(t, dbSess.User.SSOProvider) + require.NotNil(t, dbSess.User.SSOProvider.SAMLProvider) + }) + + t.Run("Successful GetUserSSOSession (OIDC)", func(t *testing.T) { + var ( + testCtx = context.Background() + dbInst, user = initAndCreateUser(t) + oidcProvider = model.OIDCProvider{ + ClientID: "bloodhound", + Issuer: "https://localhost/auth", + } + ) + + // Initialize the OIDCProvider without setting SSOProviderID + newOIDCProvider, err := dbInst.CreateOIDCProvider(testCtx, "test", oidcProvider.Issuer, oidcProvider.ClientID, model.SSOProviderConfig{}) + require.Nil(t, err) + + user.SSOProviderID = null.Int32From(int32(newOIDCProvider.SSOProviderID)) + err = dbInst.UpdateUser(testCtx, user) + require.Nil(t, err) + + userSession := model.UserSession{ + AuthProviderID: newOIDCProvider.ID, + AuthProviderType: model.SessionAuthProviderOIDC, + User: user, + UserID: user.ID, + ExpiresAt: time.Now().UTC().Add(time.Hour), + } - newUserSession, err := dbInst.CreateUserSession(testCtx, userSession) - require.Nil(t, err) + newUserSession, err := dbInst.CreateUserSession(testCtx, userSession) + require.Nil(t, err) - dbSess, err := dbInst.GetUserSession(testCtx, newUserSession.ID) - require.Nil(t, err) - require.NotNil(t, dbSess.User.SSOProvider) - require.NotNil(t, dbSess.User.SSOProvider.SAMLProvider) + dbSess, err := dbInst.GetUserSession(testCtx, newUserSession.ID) + require.Nil(t, err) + require.NotNil(t, dbSess.User.SSOProvider) + require.NotNil(t, dbSess.User.SSOProvider.OIDCProvider) + }) } diff --git a/cmd/api/src/database/migration/migrations/v6.3.0.sql b/cmd/api/src/database/migration/migrations/v6.3.0.sql index c44a919ced..bdc091fcf9 100644 --- a/cmd/api/src/database/migration/migrations/v6.3.0.sql +++ b/cmd/api/src/database/migration/migrations/v6.3.0.sql @@ -35,4 +35,4 @@ UPDATE feature_flags SET enabled = true WHERE key = 'updated_posture_page'; DELETE FROM auth_secrets WHERE id IN (SELECT auth_secrets.id FROM auth_secrets JOIN users ON users.id = auth_secrets.user_id WHERE users.sso_provider_id IS NOT NULL); -- Set the `oidc_support` feature flag to true -UPDATE feature_flags SET enabled = true WHERE key = 'oidc_support'; \ No newline at end of file +UPDATE feature_flags SET enabled = true WHERE key = 'oidc_support'; diff --git a/cmd/api/src/database/migration/migrations/v6.4.0.sql b/cmd/api/src/database/migration/migrations/v6.4.0.sql new file mode 100644 index 0000000000..e6c21f1142 --- /dev/null +++ b/cmd/api/src/database/migration/migrations/v6.4.0.sql @@ -0,0 +1,21 @@ +-- Copyright 2025 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 + +-- Add new config column in sso_providers table +ALTER TABLE IF EXISTS sso_providers ADD COLUMN IF NOT EXISTS config jsonb; + +-- Update sso_providers table by backfilling existing sso providers' new config column with default values +UPDATE sso_providers set config = '{"auto_provision": {"enabled": false, "default_role_id": 0, "role_provision": false}}'; \ No newline at end of file diff --git a/cmd/api/src/database/mocks/db.go b/cmd/api/src/database/mocks/db.go index e4514fd1d3..1129cf55a0 100644 --- a/cmd/api/src/database/mocks/db.go +++ b/cmd/api/src/database/mocks/db.go @@ -289,48 +289,48 @@ func (mr *MockDatabaseMockRecorder) CreateInstallation(arg0 interface{}) *gomock } // CreateOIDCProvider mocks base method. -func (m *MockDatabase) CreateOIDCProvider(arg0 context.Context, arg1, arg2, arg3 string) (model.OIDCProvider, error) { +func (m *MockDatabase) CreateOIDCProvider(arg0 context.Context, arg1, arg2, arg3 string, arg4 model.SSOProviderConfig) (model.OIDCProvider, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateOIDCProvider", arg0, arg1, arg2, arg3) + ret := m.ctrl.Call(m, "CreateOIDCProvider", arg0, arg1, arg2, arg3, arg4) ret0, _ := ret[0].(model.OIDCProvider) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateOIDCProvider indicates an expected call of CreateOIDCProvider. -func (mr *MockDatabaseMockRecorder) CreateOIDCProvider(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) CreateOIDCProvider(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOIDCProvider", reflect.TypeOf((*MockDatabase)(nil).CreateOIDCProvider), arg0, arg1, arg2, arg3) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateOIDCProvider", reflect.TypeOf((*MockDatabase)(nil).CreateOIDCProvider), arg0, arg1, arg2, arg3, arg4) } // CreateSAMLIdentityProvider mocks base method. -func (m *MockDatabase) CreateSAMLIdentityProvider(arg0 context.Context, arg1 model.SAMLProvider) (model.SAMLProvider, error) { +func (m *MockDatabase) CreateSAMLIdentityProvider(arg0 context.Context, arg1 model.SAMLProvider, arg2 model.SSOProviderConfig) (model.SAMLProvider, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSAMLIdentityProvider", arg0, arg1) + ret := m.ctrl.Call(m, "CreateSAMLIdentityProvider", arg0, arg1, arg2) ret0, _ := ret[0].(model.SAMLProvider) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSAMLIdentityProvider indicates an expected call of CreateSAMLIdentityProvider. -func (mr *MockDatabaseMockRecorder) CreateSAMLIdentityProvider(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) CreateSAMLIdentityProvider(arg0, arg1, arg2 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSAMLIdentityProvider", reflect.TypeOf((*MockDatabase)(nil).CreateSAMLIdentityProvider), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSAMLIdentityProvider", reflect.TypeOf((*MockDatabase)(nil).CreateSAMLIdentityProvider), arg0, arg1, arg2) } // CreateSSOProvider mocks base method. -func (m *MockDatabase) CreateSSOProvider(arg0 context.Context, arg1 string, arg2 model.SessionAuthProvider) (model.SSOProvider, error) { +func (m *MockDatabase) CreateSSOProvider(arg0 context.Context, arg1 string, arg2 model.SessionAuthProvider, arg3 model.SSOProviderConfig) (model.SSOProvider, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "CreateSSOProvider", arg0, arg1, arg2) + ret := m.ctrl.Call(m, "CreateSSOProvider", arg0, arg1, arg2, arg3) ret0, _ := ret[0].(model.SSOProvider) ret1, _ := ret[1].(error) return ret0, ret1 } // CreateSSOProvider indicates an expected call of CreateSSOProvider. -func (mr *MockDatabaseMockRecorder) CreateSSOProvider(arg0, arg1, arg2 interface{}) *gomock.Call { +func (mr *MockDatabaseMockRecorder) CreateSSOProvider(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSSOProvider", reflect.TypeOf((*MockDatabase)(nil).CreateSSOProvider), arg0, arg1, arg2) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSSOProvider", reflect.TypeOf((*MockDatabase)(nil).CreateSSOProvider), arg0, arg1, arg2, arg3) } // CreateSavedQuery mocks base method. diff --git a/cmd/api/src/database/oidc_providers.go b/cmd/api/src/database/oidc_providers.go index fdc9c0b911..1f0cbbd327 100644 --- a/cmd/api/src/database/oidc_providers.go +++ b/cmd/api/src/database/oidc_providers.go @@ -31,12 +31,12 @@ const ( // 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(ctx context.Context, name, issuer, clientID string, config model.SSOProviderConfig) (model.OIDCProvider, error) UpdateOIDCProvider(ctx context.Context, ssoProvider model.SSOProvider) (model.OIDCProvider, error) } // 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) { +func (s *BloodhoundDB) CreateOIDCProvider(ctx context.Context, name, issuer, clientID string, config model.SSOProviderConfig) (model.OIDCProvider, error) { var ( oidcProvider = model.OIDCProvider{ ClientID: clientID, @@ -54,7 +54,7 @@ func (s *BloodhoundDB) CreateOIDCProvider(ctx context.Context, name, issuer, cli err := s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { bhdb := NewBloodhoundDB(tx, s.idResolver) - if ssoProvider, err := bhdb.CreateSSOProvider(ctx, name, model.SessionAuthProviderOIDC); err != nil { + if ssoProvider, err := bhdb.CreateSSOProvider(ctx, name, model.SessionAuthProviderOIDC, config); err != nil { return err } else { oidcProvider.SSOProviderID = int(ssoProvider.ID) diff --git a/cmd/api/src/database/oidc_providers_test.go b/cmd/api/src/database/oidc_providers_test.go index dfad1db7a8..e933e47941 100644 --- a/cmd/api/src/database/oidc_providers_test.go +++ b/cmd/api/src/database/oidc_providers_test.go @@ -33,43 +33,55 @@ func TestBloodhoundDB_CreateUpdateOIDCProvider(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) + config = model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } ) + defer dbInst.Close(testCtx) - t.Run("successfully create and update an OIDC provider", func(t *testing.T) { - provider, err := dbInst.CreateOIDCProvider(testCtx, "test", "https://test.localhost.com/auth", "bloodhound") - require.NoError(t, err) + provider, err := dbInst.CreateOIDCProvider(testCtx, "test", "https://test.localhost.com/auth", "bloodhound", model.SSOProviderConfig{}) + require.NoError(t, err) - require.Equal(t, "https://test.localhost.com/auth", provider.Issuer) - require.Equal(t, "bloodhound", provider.ClientID) - require.EqualValues(t, 1, provider.ID) + require.Equal(t, "https://test.localhost.com/auth", provider.Issuer) + require.Equal(t, "bloodhound", provider.ClientID) + require.EqualValues(t, 1, provider.ID) - _, count, err := dbInst.ListAuditLogs(testCtx, time.Now().Add(time.Minute), time.Now().Add(-time.Minute), 0, 10, "", model.SQLFilter{}) - require.NoError(t, err) - require.Equal(t, 4, count) + _, count, err := dbInst.ListAuditLogs(testCtx, time.Now().Add(time.Minute), time.Now().Add(-time.Minute), 0, 10, "", model.SQLFilter{}) + require.NoError(t, err) + require.Equal(t, 4, count) - updatedSSOProvider := model.SSOProvider{ - Name: "updated provider", - Type: model.SessionAuthProviderOIDC, - OIDCProvider: &model.OIDCProvider{ - Serial: model.Serial{ - ID: provider.ID, - }, - ClientID: "gotham-net", - Issuer: "https://gotham.net", - SSOProviderID: provider.SSOProviderID, + updatedSSOProvider := model.SSOProvider{ + Serial: model.Serial{ID: 1}, + Name: "updated provider", + Type: model.SessionAuthProviderOIDC, + OIDCProvider: &model.OIDCProvider{ + Serial: model.Serial{ + ID: provider.ID, }, - } + ClientID: "gotham-net", + Issuer: "https://gotham.net", + SSOProviderID: provider.SSOProviderID, + }, + Config: config, + } + + provider, err = dbInst.UpdateOIDCProvider(testCtx, updatedSSOProvider) + require.NoError(t, err) - provider, err = dbInst.UpdateOIDCProvider(testCtx, updatedSSOProvider) - require.NoError(t, err) + require.Equal(t, updatedSSOProvider.OIDCProvider.Issuer, provider.Issuer) + require.Equal(t, updatedSSOProvider.OIDCProvider.ClientID, provider.ClientID) + require.EqualValues(t, updatedSSOProvider.OIDCProvider.ID, provider.ID) - require.Equal(t, updatedSSOProvider.OIDCProvider.Issuer, provider.Issuer) - require.Equal(t, updatedSSOProvider.OIDCProvider.ClientID, provider.ClientID) - require.EqualValues(t, updatedSSOProvider.OIDCProvider.ID, provider.ID) + ssoProvider, err := dbInst.GetSSOProviderById(testCtx, int32(provider.SSOProviderID)) + require.Nil(t, err) + require.Equal(t, updatedSSOProvider.Config, ssoProvider.Config) - _, count, err = dbInst.ListAuditLogs(testCtx, time.Now().Add(time.Minute), time.Now().Add(-time.Minute), 0, 10, "", model.SQLFilter{}) - require.NoError(t, err) - require.Equal(t, 8, count) - }) + _, count, err = dbInst.ListAuditLogs(testCtx, time.Now().Add(time.Minute), time.Now().Add(-time.Minute), 0, 10, "", model.SQLFilter{}) + require.NoError(t, err) + require.Equal(t, 8, count) } diff --git a/cmd/api/src/database/samlproviders.go b/cmd/api/src/database/samlproviders.go index 73df1197a5..adcaf1a6e0 100644 --- a/cmd/api/src/database/samlproviders.go +++ b/cmd/api/src/database/samlproviders.go @@ -32,7 +32,7 @@ const ( // SAMLProviderData defines the interface required to interact with the oidc_providers table type SAMLProviderData interface { - CreateSAMLIdentityProvider(ctx context.Context, samlProvider model.SAMLProvider) (model.SAMLProvider, error) + CreateSAMLIdentityProvider(ctx context.Context, samlProvider model.SAMLProvider, config model.SSOProviderConfig) (model.SAMLProvider, error) GetAllSAMLProviders(ctx context.Context) (model.SAMLProviders, error) GetSAMLProvider(ctx context.Context, id int32) (model.SAMLProvider, error) GetSAMLProviderUsers(ctx context.Context, id int32) (model.Users, error) @@ -42,7 +42,7 @@ type SAMLProviderData interface { // CreateSAMLIdentityProvider creates a new saml_providers row using the data in the input struct // This also creates the corresponding sso_provider entry // INSERT INTO saml_identity_providers (...) VALUES (...) -func (s *BloodhoundDB) CreateSAMLIdentityProvider(ctx context.Context, samlProvider model.SAMLProvider) (model.SAMLProvider, error) { +func (s *BloodhoundDB) CreateSAMLIdentityProvider(ctx context.Context, samlProvider model.SAMLProvider, config model.SSOProviderConfig) (model.SAMLProvider, error) { // Set the current version for root_uri_version samlProvider.RootURIVersion = model.SAMLRootURIVersion2 @@ -55,7 +55,7 @@ func (s *BloodhoundDB) CreateSAMLIdentityProvider(ctx context.Context, samlProvi bhdb := NewBloodhoundDB(tx, s.idResolver) // Create the associated SSO provider - if ssoProvider, err := bhdb.CreateSSOProvider(ctx, samlProvider.Name, model.SessionAuthProviderSAML); err != nil { + if ssoProvider, err := bhdb.CreateSSOProvider(ctx, samlProvider.Name, model.SessionAuthProviderSAML, config); err != nil { return err } else { samlProvider.SSOProviderID = null.Int32From(ssoProvider.ID) diff --git a/cmd/api/src/database/sso_providers.go b/cmd/api/src/database/sso_providers.go index f820bebd3f..932a66ae9b 100644 --- a/cmd/api/src/database/sso_providers.go +++ b/cmd/api/src/database/sso_providers.go @@ -33,7 +33,7 @@ const ( // SSOProviderData defines the methods required to interact with the sso_providers table type SSOProviderData interface { - CreateSSOProvider(ctx context.Context, name string, authProvider model.SessionAuthProvider) (model.SSOProvider, error) + CreateSSOProvider(ctx context.Context, name string, authProvider model.SessionAuthProvider, config model.SSOProviderConfig) (model.SSOProvider, error) DeleteSSOProvider(ctx context.Context, id int) error GetAllSSOProviders(ctx context.Context, order string, sqlFilter model.SQLFilter) ([]model.SSOProvider, error) GetSSOProviderById(ctx context.Context, id int32) (model.SSOProvider, error) @@ -45,12 +45,18 @@ type SSOProviderData interface { // 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, authProvider model.SessionAuthProvider) (model.SSOProvider, error) { +func (s *BloodhoundDB) CreateSSOProvider(ctx context.Context, name string, authProvider model.SessionAuthProvider, config model.SSOProviderConfig) (model.SSOProvider, error) { + // If we have a disabled autoprovision, wipe the auto provision config + if !config.AutoProvision.Enabled { + config.AutoProvision = model.SSOProviderAutoProvisionConfig{} + } + var ( provider = model.SSOProvider{ - Name: name, - Slug: strings.ToLower(strings.ReplaceAll(name, " ", "-")), - Type: authProvider, + Name: name, + Slug: strings.ToLower(strings.ReplaceAll(name, " ", "-")), + Type: authProvider, + Config: config, } auditEntry = model.AuditEntry{ @@ -192,8 +198,13 @@ func (s *BloodhoundDB) UpdateSSOProvider(ctx context.Context, ssoProvider model. Model: &ssoProvider, } + // If we have a disabled autoprovision, wipe the auto provision config + if !ssoProvider.Config.AutoProvision.Enabled { + ssoProvider.Config.AutoProvision = model.SSOProviderAutoProvisionConfig{} + } + err := s.AuditableTransaction(ctx, auditEntry, func(tx *gorm.DB) error { - result := tx.WithContext(ctx).Exec(fmt.Sprintf("UPDATE %s SET name = ?, slug = ?, updated_at = ? WHERE id = ?;", ssoProviderTableName), ssoProvider.Name, ssoProvider.Slug, time.Now().UTC(), ssoProvider.ID) + result := tx.WithContext(ctx).Exec(fmt.Sprintf("UPDATE %s SET name = ?, slug = ?, updated_at = ?, config = ? WHERE id = ?;", ssoProviderTableName), ssoProvider.Name, ssoProvider.Slug, time.Now().UTC(), ssoProvider.Config, ssoProvider.ID) if result.Error != nil { if strings.Contains(result.Error.Error(), "duplicate key value violates unique constraint \"sso_providers_name_key\"") { diff --git a/cmd/api/src/database/sso_providers_test.go b/cmd/api/src/database/sso_providers_test.go index 899bcebe28..a402163329 100644 --- a/cmd/api/src/database/sso_providers_test.go +++ b/cmd/api/src/database/sso_providers_test.go @@ -38,8 +38,8 @@ func TestBloodhoundDB_CreateAndGetSSOProvider(t *testing.T) { ) defer dbInst.Close(testCtx) - t.Run("successfully create an SSO provider", func(t *testing.T) { - result, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang", model.SessionAuthProviderSAML) + t.Run("successfully create an SSO provider (SAML)", func(t *testing.T) { + result, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang", model.SessionAuthProviderSAML, model.SSOProviderConfig{}) require.NoError(t, err) assert.Equal(t, "Bloodhound Gang", result.Name) @@ -47,6 +47,58 @@ func TestBloodhoundDB_CreateAndGetSSOProvider(t *testing.T) { assert.Equal(t, model.SessionAuthProviderSAML, result.Type) assert.NotEmpty(t, result.ID) }) + + t.Run("successfully created an SSO provider with config values (SAML)", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + result, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang2", model.SessionAuthProviderSAML, config) + require.NoError(t, err) + + assert.Equal(t, "Bloodhound Gang2", result.Name) + assert.Equal(t, "bloodhound-gang2", result.Slug) + assert.Equal(t, model.SessionAuthProviderSAML, result.Type) + assert.Equal(t, true, result.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), result.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, result.Config.AutoProvision.RoleProvision) + assert.NotEmpty(t, result.ID) + }) + + t.Run("successfully create an SSO provider (OIDC)", func(t *testing.T) { + result, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang3", model.SessionAuthProviderOIDC, model.SSOProviderConfig{}) + require.NoError(t, err) + + assert.Equal(t, "Bloodhound Gang3", result.Name) + assert.Equal(t, "bloodhound-gang3", result.Slug) + assert.Equal(t, model.SessionAuthProviderOIDC, result.Type) + assert.NotEmpty(t, result.ID) + }) + + t.Run("successfully created an SSO provider with config values (OIDC)", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + result, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang4", model.SessionAuthProviderOIDC, config) + require.NoError(t, err) + + assert.Equal(t, "Bloodhound Gang4", result.Name) + assert.Equal(t, "bloodhound-gang4", result.Slug) + assert.Equal(t, model.SessionAuthProviderOIDC, result.Type) + assert.Equal(t, true, result.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), result.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, result.Config.AutoProvision.RoleProvision) + assert.NotEmpty(t, result.ID) + }) } func TestBloodhoundDB_DeleteSSOProvider(t *testing.T) { @@ -57,7 +109,7 @@ func TestBloodhoundDB_DeleteSSOProvider(t *testing.T) { defer dbInst.Close(testCtx) t.Run("successfully delete an SSO provider associated with a SAML provider", func(t *testing.T) { - samlProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, model.SAMLProvider{Name: "test"}) + samlProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, model.SAMLProvider{Name: "test"}, model.SSOProviderConfig{}) require.NoError(t, err) user, err := dbInst.CreateUser(testCtx, model.User{ @@ -74,13 +126,65 @@ func TestBloodhoundDB_DeleteSSOProvider(t *testing.T) { assert.Equal(t, null.NewInt32(0, false), user.SSOProviderID) }) + t.Run("successfully delete an SSO provider associated with a SAML provider with config values", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + samlProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, model.SAMLProvider{Name: "test2"}, config) + require.NoError(t, err) + + user, err := dbInst.CreateUser(testCtx, model.User{ + SSOProviderID: samlProvider.SSOProviderID, + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + err = dbInst.DeleteSSOProvider(testCtx, int(samlProvider.SSOProviderID.Int32)) + require.NoError(t, err) + + user, err = dbInst.GetUser(testCtx, user.ID) + require.NoError(t, err) + assert.Equal(t, null.NewInt32(0, false), user.SSOProviderID) + }) + t.Run("successfully delete an SSO provider associated with an OIDC provider", func(t *testing.T) { - oidcProvider, err := dbInst.CreateOIDCProvider(testCtx, "test", "test", "test") + oidcProvider, err := dbInst.CreateOIDCProvider(testCtx, "test3", "test3", "test3", model.SSOProviderConfig{}) require.NoError(t, err) user, err := dbInst.CreateUser(testCtx, model.User{ SSOProviderID: null.Int32From(int32(oidcProvider.SSOProviderID)), - PrincipalName: user2Principal, + PrincipalName: user3Principal, + }) + require.NoError(t, err) + + err = dbInst.DeleteSSOProvider(testCtx, oidcProvider.SSOProviderID) + require.NoError(t, err) + + user, err = dbInst.GetUser(testCtx, user.ID) + require.NoError(t, err) + assert.Equal(t, null.NewInt32(0, false), user.SSOProviderID) + }) + + t.Run("successfully delete an SSO provider associated with an OIDC provider with config values", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + oidcProvider, err := dbInst.CreateOIDCProvider(testCtx, "test4", "test4", "test4", config) + require.NoError(t, err) + + user, err := dbInst.CreateUser(testCtx, model.User{ + SSOProviderID: null.Int32From(int32(oidcProvider.SSOProviderID)), + PrincipalName: user4Principal, }) require.NoError(t, err) @@ -102,10 +206,10 @@ func TestBloodhoundDB_GetAllSSOProviders(t *testing.T) { t.Run("successfully list SSO providers with and without sorting", func(t *testing.T) { // Create SSO providers - provider1, err := dbInst.CreateSSOProvider(testCtx, "First Provider", model.SessionAuthProviderSAML) + provider1, err := dbInst.CreateSSOProvider(testCtx, "First Provider", model.SessionAuthProviderSAML, model.SSOProviderConfig{}) require.NoError(t, err) - provider2, err := dbInst.CreateSSOProvider(testCtx, "Second Provider", model.SessionAuthProviderOIDC) + provider2, err := dbInst.CreateSSOProvider(testCtx, "Second Provider", model.SessionAuthProviderOIDC, model.SSOProviderConfig{}) require.NoError(t, err) // Enable the OIDC feature flag @@ -140,17 +244,74 @@ func TestBloodhoundDB_GetAllSSOProviders(t *testing.T) { require.Len(t, providers, 1) assert.Equal(t, provider1.ID, providers[0].ID) }) + + // This test fails individually, but passes when ran together with the other tests + t.Run("successfully list SSO providers with and without sorting (with configs)", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + // Create SSO providers + provider3, err := dbInst.CreateSSOProvider(testCtx, "Third Provider", model.SessionAuthProviderSAML, config) + require.NoError(t, err) + assert.Equal(t, true, provider3.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider3.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider3.Config.AutoProvision.RoleProvision) + + provider4, err := dbInst.CreateSSOProvider(testCtx, "Fourth Provider", model.SessionAuthProviderOIDC, config) + require.NoError(t, err) + assert.Equal(t, true, provider4.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider4.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider4.Config.AutoProvision.RoleProvision) + + // Enable the OIDC feature flag + oidcFlag, err := dbInst.GetFlagByKey(testCtx, appcfg.FeatureOIDCSupport) + require.NoError(t, err) + oidcFlag.Enabled = true + + err = dbInst.SetFlag(testCtx, oidcFlag) + require.NoError(t, err) + + // Test default ordering (by created_at) + providers, err := dbInst.GetAllSSOProviders(testCtx, "", model.SQLFilter{}) + require.NoError(t, err) + require.Len(t, providers, 4) + assert.Equal(t, provider3.ID, providers[2].ID) + assert.Equal(t, provider4.ID, providers[3].ID) + + // Test ordering by name descending + providers, err = dbInst.GetAllSSOProviders(testCtx, "name desc", model.SQLFilter{}) + require.NoError(t, err) + require.Len(t, providers, 4) + assert.Equal(t, provider3.ID, providers[0].ID) + assert.Equal(t, provider4.ID, providers[2].ID) + + // Test filtering by name + sqlFilter := model.SQLFilter{ + SQLString: "name = ?", + Params: []interface{}{"Third Provider"}, + } + providers, err = dbInst.GetAllSSOProviders(testCtx, "", sqlFilter) + require.NoError(t, err) + require.Len(t, providers, 1) + assert.Equal(t, provider3.ID, providers[0].ID) + }) } func TestBloodhoundDB_GetSSOProviderBySlug(t *testing.T) { var ( testCtx = context.Background() dbInst = integration.SetupDB(t) + config = model.SSOProviderConfig{} ) defer dbInst.Close(testCtx) - t.Run("successfully get sso provider by slug", func(t *testing.T) { - newProvider, err := dbInst.CreateOIDCProvider(testCtx, "Gotham Net", "https://test.localhost.com/auth", "gotham-net") + t.Run("successfully get sso provider by slug (OIDC)", func(t *testing.T) { + newProvider, err := dbInst.CreateOIDCProvider(testCtx, "Gotham Net", "https://test.localhost.com/auth", "gotham-net", config) require.Nil(t, err) provider, err := dbInst.GetSSOProviderBySlug(testCtx, "gotham-net") @@ -160,6 +321,29 @@ func TestBloodhoundDB_GetSSOProviderBySlug(t *testing.T) { require.Equal(t, newProvider.ClientID, provider.OIDCProvider.ClientID) require.Equal(t, newProvider.Issuer, provider.OIDCProvider.Issuer) }) + + t.Run("successfully get sso provider by slug (OIDC) with configs", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + newProvider, err := dbInst.CreateOIDCProvider(testCtx, "Gotham Net2", "https://test.localhost.com/auth", "gotham-net2", config) + require.Nil(t, err) + + provider, err := dbInst.GetSSOProviderBySlug(testCtx, "gotham-net2") + require.Nil(t, err) + require.EqualValues(t, newProvider.SSOProviderID, provider.ID) + require.NotNil(t, provider.OIDCProvider) + require.Equal(t, newProvider.ClientID, provider.OIDCProvider.ClientID) + require.Equal(t, newProvider.Issuer, provider.OIDCProvider.Issuer) + assert.Equal(t, true, provider.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider.Config.AutoProvision.RoleProvision) + }) } func TestBloodhoundDB_GetSSOProviderUsers(t *testing.T) { @@ -169,12 +353,86 @@ func TestBloodhoundDB_GetSSOProviderUsers(t *testing.T) { ) defer dbInst.Close(testCtx) - t.Run("successfully list SSO provider users", func(t *testing.T) { - provider, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang", model.SessionAuthProviderSAML) + t.Run("successfully list SSO provider users (SAML)", func(t *testing.T) { + provider, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang", model.SessionAuthProviderSAML, model.SSOProviderConfig{}) require.NoError(t, err) user, err := dbInst.CreateUser(testCtx, model.User{ SSOProviderID: null.Int32From(provider.ID), + PrincipalName: userPrincipal, + }) + require.NoError(t, err) + + returnedUsers, err := dbInst.GetSSOProviderUsers(testCtx, int(provider.ID)) + require.NoError(t, err) + + require.Len(t, returnedUsers, 1) + assert.Equal(t, user.ID, returnedUsers[0].ID) + }) + + t.Run("successfully list SSO provider users (SAML) with configs", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + provider, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang2", model.SessionAuthProviderSAML, config) + require.NoError(t, err) + assert.Equal(t, true, provider.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider.Config.AutoProvision.RoleProvision) + + user, err := dbInst.CreateUser(testCtx, model.User{ + SSOProviderID: null.Int32From(provider.ID), + PrincipalName: user2Principal, + }) + require.NoError(t, err) + + returnedUsers, err := dbInst.GetSSOProviderUsers(testCtx, int(provider.ID)) + require.NoError(t, err) + + require.Len(t, returnedUsers, 1) + assert.Equal(t, user.ID, returnedUsers[0].ID) + }) + + t.Run("successfully list SSO provider users (OIDC)", func(t *testing.T) { + provider, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang3", model.SessionAuthProviderOIDC, model.SSOProviderConfig{}) + require.NoError(t, err) + + user, err := dbInst.CreateUser(testCtx, model.User{ + SSOProviderID: null.Int32From(provider.ID), + PrincipalName: user3Principal, + }) + require.NoError(t, err) + + returnedUsers, err := dbInst.GetSSOProviderUsers(testCtx, int(provider.ID)) + require.NoError(t, err) + + require.Len(t, returnedUsers, 1) + assert.Equal(t, user.ID, returnedUsers[0].ID) + }) + + t.Run("successfully list SSO provider users (OIDC) with configs", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + + provider, err := dbInst.CreateSSOProvider(testCtx, "Bloodhound Gang4", model.SessionAuthProviderOIDC, config) + require.NoError(t, err) + assert.Equal(t, true, provider.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider.Config.AutoProvision.RoleProvision) + + user, err := dbInst.CreateUser(testCtx, model.User{ + SSOProviderID: null.Int32From(provider.ID), + PrincipalName: user4Principal, }) require.NoError(t, err) @@ -193,17 +451,83 @@ func TestBloodhoundDB_GetSSOProviderById(t *testing.T) { ) defer dbInst.Close(testCtx) - t.Run("successfully get sso provider by id", func(t *testing.T) { + t.Run("successfully get sso provider by id (SAML)", func(t *testing.T) { newSamlProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, model.SAMLProvider{ Name: "someName", DisplayName: "someName", - }) + }, model.SSOProviderConfig{}) + require.NoError(t, err) + + provider, err := dbInst.GetSSOProviderById(testCtx, newSamlProvider.SSOProviderID.Int32) + require.NoError(t, err) + + require.EqualValues(t, newSamlProvider.SSOProviderID.Int32, provider.ID) + require.NotNil(t, provider.SAMLProvider) + }) + + t.Run("successfully get sso provider by id with config values (SAML)", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + newSamlProvider, err := dbInst.CreateSAMLIdentityProvider(testCtx, model.SAMLProvider{ + Name: "someName2", + DisplayName: "someName2", + }, config) require.NoError(t, err) provider, err := dbInst.GetSSOProviderById(testCtx, newSamlProvider.SSOProviderID.Int32) require.NoError(t, err) + assert.Equal(t, true, provider.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider.Config.AutoProvision.RoleProvision) require.EqualValues(t, newSamlProvider.SSOProviderID.Int32, provider.ID) require.NotNil(t, provider.SAMLProvider) }) + + t.Run("successfully get sso provider by id (OIDC)", func(t *testing.T) { + oidcProvider := model.OIDCProvider{ + ClientID: "bloodhound", + Issuer: "https://localhost/auth", + } + + newOIDCProvider, err := dbInst.CreateOIDCProvider(testCtx, "test", oidcProvider.Issuer, oidcProvider.ClientID, model.SSOProviderConfig{}) + require.Nil(t, err) + + provider, err := dbInst.GetSSOProviderById(testCtx, int32(newOIDCProvider.SSOProviderID)) + require.NoError(t, err) + + require.EqualValues(t, int32(newOIDCProvider.SSOProviderID), provider.ID) + require.NotNil(t, provider.OIDCProvider) + }) + + t.Run("successfully get sso provider by id with config values (OIDC)", func(t *testing.T) { + config := model.SSOProviderConfig{ + AutoProvision: model.SSOProviderAutoProvisionConfig{ + Enabled: true, + DefaultRoleId: 3, + RoleProvision: true, + }, + } + oidcProvider := model.OIDCProvider{ + ClientID: "bloodhound2", + Issuer: "https://localhost/auth", + } + + newOIDCProvider, err := dbInst.CreateOIDCProvider(testCtx, "test2", oidcProvider.Issuer, oidcProvider.ClientID, config) + require.Nil(t, err) + + provider, err := dbInst.GetSSOProviderById(testCtx, int32(newOIDCProvider.SSOProviderID)) + require.NoError(t, err) + assert.Equal(t, true, provider.Config.AutoProvision.Enabled) + assert.Equal(t, int32(3), provider.Config.AutoProvision.DefaultRoleId) + assert.Equal(t, true, provider.Config.AutoProvision.RoleProvision) + + require.EqualValues(t, int32(newOIDCProvider.SSOProviderID), provider.ID) + require.NotNil(t, provider.OIDCProvider) + }) } diff --git a/cmd/api/src/model/samlprovider.go b/cmd/api/src/model/samlprovider.go index 6cffc17bdc..7d97427139 100644 --- a/cmd/api/src/model/samlprovider.go +++ b/cmd/api/src/model/samlprovider.go @@ -30,8 +30,15 @@ import ( const ( ObjectIDAttributeNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri" ObjectIDEmail = "urn:oid:0.9.2342.19200300.100.1.3" - XMLTypeString = "xs:string" - XMLSOAPClaimsEmailAddress = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + ObjectIDGivenName = "urn:oid:2.5.4.42" + ObjectIDName = "urn:oid:2.5.4.41" + ObjectIDSurname = "urn:oid:2.5.4.4" + + XMLTypeString = "xs:string" + XMLSOAPClaimsEmailAddress = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" + XMLSOAPClaimsGivenName = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" + XMLSOAPClaimsName = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" + XMLSOAPClaimsSurname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" ) var ( @@ -106,6 +113,15 @@ func (s SAMLProvider) emailAttributeNames() []string { return []string{ObjectIDEmail, XMLSOAPClaimsEmailAddress} } +func (s SAMLProvider) givenNameAttributeNames() []string { + // Added the ObjectIDName and XMLSOAPClaimsName as a fallback + return []string{ObjectIDGivenName, XMLSOAPClaimsGivenName, ObjectIDName, XMLSOAPClaimsName} +} + +func (s SAMLProvider) surnameAttributeNames() []string { + return []string{ObjectIDSurname, XMLSOAPClaimsSurname} +} + func assertionFindString(assertion *saml.Assertion, names ...string) (string, error) { for _, attributeStatement := range assertion.AttributeStatements { for _, attribute := range attributeStatement.Attributes { @@ -145,6 +161,14 @@ func (s SAMLProvider) GetSAMLUserPrincipalNameFromAssertion(assertion *saml.Asse } } +func (s SAMLProvider) GetSAMLUserGivenNameFromAssertion(assertion *saml.Assertion) (string, error) { + return assertionFindString(assertion, s.givenNameAttributeNames()...) +} + +func (s SAMLProvider) GetSAMLUserSurnameFromAssertion(assertion *saml.Assertion) (string, error) { + return assertionFindString(assertion, s.surnameAttributeNames()...) +} + func (s *SAMLProvider) FormatSAMLProviderURLs(hostUrl url.URL) { root := hostUrl root.Path = path.Join(SAMLRootURIVersionMap[s.RootURIVersion], s.Name) diff --git a/cmd/api/src/model/sso_provider.go b/cmd/api/src/model/sso_provider.go index 61c64cfdac..fdad49fb87 100644 --- a/cmd/api/src/model/sso_provider.go +++ b/cmd/api/src/model/sso_provider.go @@ -16,7 +16,22 @@ package model -import "fmt" +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type SSOProviderAutoProvisionConfig struct { + Enabled bool `json:"enabled"` + DefaultRoleId int32 `json:"default_role_id"` + RoleProvision bool `json:"role_provision"` +} + +type SSOProviderConfig struct { + AutoProvision SSOProviderAutoProvisionConfig `json:"auto_provision"` +} // SSOProvider is the common representation of an SSO provider that can be used to display high level information about that provider type SSOProvider struct { @@ -27,9 +42,33 @@ type SSOProvider struct { OIDCProvider *OIDCProvider `json:"oidc_provider,omitempty" gorm:"foreignKey:SSOProviderID"` SAMLProvider *SAMLProvider `json:"saml_provider,omitempty" gorm:"foreignKey:SSOProviderID"` + Config SSOProviderConfig `json:"config" gorm:"type:jsonb column:config"` + Serial } +// Implement the sql.Scanner interface so that GORM can scan the jsonb column from the database into a golang struct +func (cfg *SSOProviderConfig) Scan(value interface{}) error { + // Handle null values from the database + if value == nil { + *cfg = SSOProviderConfig{} + return nil + } + + // Convert the database value to []byte + if bytes, ok := value.([]byte); !ok { + return errors.New("type assertion to []byte failed for SSOProviderConfig") + } else { + // Unmarshal JSON into the struct + return json.Unmarshal(bytes, cfg) + } +} + +// Value returns the json-marshaled value of the receiver +func (cfg SSOProviderConfig) Value() (driver.Value, error) { + return json.Marshal(cfg) +} + // AuditData returns the fields to log in the audit log func (s SSOProvider) AuditData() AuditData { var ( diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index c36f1345f5..b4f8c0b54d 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -580,7 +580,8 @@ "required": [ "name", "issuer", - "client_id" + "client_id", + "config" ], "properties": { "name": { @@ -595,6 +596,29 @@ "client_id": { "type": "string", "description": "Client ID for the OIDC provider" + }, + "config": { + "type": "object", + "properties": { + "auto_provision": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login" + }, + "default_role_id": { + "type": "integer", + "format": "int32", + "description": "default role id for the user created from SSO provider auto provision" + }, + "role_provision": { + "type": "boolean", + "description": "boolean that, if enabled, allows sso providers to manage roles for newly created users" + } + } + } + } } } } @@ -650,6 +674,13 @@ "content": { "multipart/form-data": { "schema": { + "required": [ + "name", + "metadata", + "config.auto_provision.enabled", + "config.auto_provision.default_role_id", + "config.auto_provision.role_provision" + ], "properties": { "name": { "type": "string", @@ -659,6 +690,21 @@ "type": "string", "format": "binary", "description": "Metadata XML file." + }, + "config.auto_provision.enabled": { + "type": "string", + "example": "true", + "description": "boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login" + }, + "config.auto_provision.default_role_id": { + "type": "string", + "example": "3", + "description": "default role id for the user created from SSO provider auto provision" + }, + "config.auto_provision.role_provision": { + "type": "string", + "example": "false", + "description": "boolean that, if enabled, allows sso providers to manage roles for newly created users" } } } @@ -738,6 +784,21 @@ "type": "string", "format": "binary", "description": "Metadata XML file." + }, + "config.auto_provision.enabled": { + "type": "string", + "example": "true", + "description": "boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login" + }, + "config.auto_provision.default_role_id": { + "type": "string", + "example": "3", + "description": "default role id for the user created from SSO provider auto provision" + }, + "config.auto_provision.role_provision": { + "type": "string", + "example": "false", + "description": "boolean that, if enabled, allows sso providers to manage roles for newly created users" } } } @@ -758,6 +819,29 @@ "client_id": { "type": "string", "description": "Client ID for the OIDC provider" + }, + "config": { + "type": "object", + "properties": { + "auto_provision": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login" + }, + "default_role_id": { + "type": "integer", + "format": "int32", + "description": "default role id for the user created from SSO provider auto provision" + }, + "role_provision": { + "type": "boolean", + "description": "boolean that, if enabled, allows sso providers to manage roles for newly created users" + } + } + } + } } } } diff --git a/packages/go/openapi/src/paths/auth.sso-providers.saml.yaml b/packages/go/openapi/src/paths/auth.sso-providers.saml.yaml index 70c254f007..6b2e8a618f 100644 --- a/packages/go/openapi/src/paths/auth.sso-providers.saml.yaml +++ b/packages/go/openapi/src/paths/auth.sso-providers.saml.yaml @@ -27,6 +27,12 @@ post: content: multipart/form-data: schema: + required: + - name + - metadata + - config.auto_provision.enabled + - config.auto_provision.default_role_id + - config.auto_provision.role_provision properties: name: type: string @@ -35,6 +41,19 @@ post: type: string format: binary description: Metadata XML file. + config.auto_provision.enabled: + type: string + example: "true" + description: boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login + config.auto_provision.default_role_id: + type: string + example: "3" + description: default role id for the user created from SSO provider auto provision + config.auto_provision.role_provision: + type: string + example: "false" + description: boolean that, if enabled, allows sso providers to manage roles for newly created users + responses: 200: description: OK diff --git a/packages/go/openapi/src/paths/sso.sso-providers.id.yaml b/packages/go/openapi/src/paths/sso.sso-providers.id.yaml index 0a82e816fc..67d1943cf2 100644 --- a/packages/go/openapi/src/paths/sso.sso-providers.id.yaml +++ b/packages/go/openapi/src/paths/sso.sso-providers.id.yaml @@ -44,6 +44,18 @@ patch: type: string format: binary description: Metadata XML file. + config.auto_provision.enabled: + type: string + example: "true" + description: boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login + config.auto_provision.default_role_id: + type: string + example: "3" + description: default role id for the user created from SSO provider auto provision + config.auto_provision.role_provision: + type: string + example: "false" + description: boolean that, if enabled, allows sso providers to manage roles for newly created users application/json: schema: type: object @@ -58,6 +70,22 @@ patch: client_id: type: string description: Client ID for the OIDC provider + config: + type: object + properties: + auto_provision: + type: object + properties: + enabled: + type: boolean + description: boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login + default_role_id: + type: integer + format: int32 + description: default role id for the user created from SSO provider auto provision + role_provision: + type: boolean + description: boolean that, if enabled, allows sso providers to manage roles for newly created users responses: '200': description: OK diff --git a/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml b/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml index 777ad47cdc..19e0995fe0 100644 --- a/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml +++ b/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml @@ -34,6 +34,7 @@ post: - name - issuer - client_id + - config properties: name: type: string @@ -45,6 +46,25 @@ post: client_id: type: string description: Client ID for the OIDC provider + config: + type: object + properties: + auto_provision: + type: object + properties: + enabled: + type: boolean + description: boolean that, if enabled, allows SSO providers to auto provision bloodhound users on initial login + default_role_id: + type: integer + format: int32 + description: default role id for the user created from SSO provider auto provision + role_provision: + type: boolean + description: boolean that, if enabled, allows sso providers to manage roles for newly created users + + + responses: '201': description: OK From 19089bc28e7f935f715a423fe9cf520da3fbba1b Mon Sep 17 00:00:00 2001 From: mistahj67 <26472282+mistahj67@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:28:22 -1000 Subject: [PATCH 7/9] Bed-5008 feat: Add role provision support (#1043) --- cmd/api/src/api/v2/auth/oidc.go | 40 ++++++++++--------- cmd/api/src/api/v2/auth/saml.go | 32 +++++++-------- cmd/api/src/api/v2/auth/sso.go | 60 ++++++++++++++++++++++------- cmd/api/src/api/v2/auth/sso_test.go | 49 +++++++++++++++++++++++ cmd/api/src/api/v2/helpers.go | 16 ++++++++ cmd/api/src/model/auth.go | 10 +++++ cmd/api/src/model/samlprovider.go | 23 +++++++++++ 7 files changed, 184 insertions(+), 46 deletions(-) diff --git a/cmd/api/src/api/v2/auth/oidc.go b/cmd/api/src/api/v2/auth/oidc.go index 3cf32b1ede..1dbfccb116 100644 --- a/cmd/api/src/api/v2/auth/oidc.go +++ b/cmd/api/src/api/v2/auth/oidc.go @@ -28,6 +28,7 @@ import ( "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" + "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/config" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" @@ -45,11 +46,12 @@ var ( ) type oidcClaims struct { - Name string `json:"name"` - FamilyName string `json:"family_name"` - DisplayName string `json:"given_name"` - Email string `json:"email"` - Verified bool `json:"email_verified"` + Name string `json:"name"` + FamilyName string `json:"family_name"` + DisplayName string `json:"given_name"` + Email string `json:"email"` + Verified bool `json:"email_verified"` + Roles []string `json:"roles"` } // UpsertOIDCProviderRequest represents the body of create & update provider endpoints @@ -148,16 +150,16 @@ func (s ManagementResource) OIDCLoginHandler(response http.ResponseWriter, reque if ssoProvider.OIDCProvider == nil { // SSO misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if state, err := config.GenerateRandomBase64String(77); err != nil { log.Errorf("[OIDC] Failed to generate state: %v", err) // Technical issues scenario - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if provider, err := oidc.NewProvider(request.Context(), ssoProvider.OIDCProvider.Issuer); err != nil { log.Errorf("[OIDC] Failed to create OIDC provider: %v", err) // SSO misconfiguration or technical issue // Treat this as a misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { conf := &oauth2.Config{ ClientID: ssoProvider.OIDCProvider.ClientID, @@ -193,27 +195,27 @@ func (s ManagementResource) OIDCCallbackHandler(response http.ResponseWriter, re if ssoProvider.OIDCProvider == nil { // SSO misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if len(code) == 0 { // Missing authorization code implies a credentials or form issue // Not explicitly covered, treat as technical issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if pkceVerifier, err := request.Cookie(api.AuthPKCECookieName); err != nil { // Missing PKCE verifier - likely a technical or config issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if len(state) == 0 { // Missing state parameter - treat as technical issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if stateCookie, err := request.Cookie(api.AuthStateCookieName); err != nil || stateCookie.Value != state[0] { // Invalid state - treat as technical issue or misconfiguration - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if provider, err := oidc.NewProvider(request.Context(), ssoProvider.OIDCProvider.Issuer); err != nil { log.Errorf("[OIDC] Failed to create OIDC provider: %v", err) // SSO misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if claims, err := getOIDCClaims(request.Context(), provider, ssoProvider, pkceVerifier, code[0]); err != nil { log.Errorf("[OIDC] %v", err) - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { if ssoProvider.Config.AutoProvision.Enabled { if err := jitOIDCUserCreation(request.Context(), ssoProvider, claims, s.db); err != nil { @@ -254,15 +256,17 @@ func getOIDCClaims(reqCtx context.Context, provider *oidc.Provider, ssoProvider } func jitOIDCUserCreation(ctx context.Context, ssoProvider model.SSOProvider, claims oidcClaims, u jitUserCreator) error { - if role, err := u.GetRole(ctx, ssoProvider.Config.AutoProvision.DefaultRoleId); err != nil { - return fmt.Errorf("get role: %v", err) + if roles, err := SanitizeAndGetRoles(ctx, ssoProvider.Config.AutoProvision, claims.Roles, u); err != nil { + return fmt.Errorf("sanitize roles: %v", err) + } else if len(roles) != 1 { + return fmt.Errorf("invalid roles") } else if _, err := u.LookupUser(ctx, claims.Email); err != nil && !errors.Is(err, database.ErrNotFound) { return fmt.Errorf("lookup user: %v", err) } else if errors.Is(err, database.ErrNotFound) { var user = model.User{ EmailAddress: null.StringFrom(claims.Email), PrincipalName: claims.Email, - Roles: model.Roles{role}, + Roles: roles, SSOProviderID: null.Int32From(ssoProvider.ID), EULAAccepted: true, // EULA Acceptance does not pertain to Bloodhound Community Edition; this flag is used for Bloodhound Enterprise users FirstName: null.StringFrom(claims.Email), diff --git a/cmd/api/src/api/v2/auth/saml.go b/cmd/api/src/api/v2/auth/saml.go index 70aab79af2..15c7bdf974 100644 --- a/cmd/api/src/api/v2/auth/saml.go +++ b/cmd/api/src/api/v2/auth/saml.go @@ -35,7 +35,7 @@ import ( "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/mediatypes" "github.com/specterops/bloodhound/src/api" - v2 "github.com/specterops/bloodhound/src/api/v2" + "github.com/specterops/bloodhound/src/api/v2" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/database" @@ -379,12 +379,12 @@ func (s ManagementResource) ServeSigningCertificate(response http.ResponseWriter func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { if ssoProvider.SAMLProvider == nil { // SAML misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if serviceProvider, err := auth.NewServiceProvider(*ctx.Get(request.Context()).Host, s.config, *ssoProvider.SAMLProvider); err != nil { log.Errorf("[SAML] Service provider creation failed: %v", err) // Technical issues scenario - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else { var ( binding = saml.HTTPRedirectBinding @@ -400,14 +400,14 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque log.Errorf("[SAML] Failed creating SAML authentication request: %v", err) // SAML misconfiguration or technical issue // Since this likely indicates a configuration problem, we treat it as a misconfiguration scenario - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { switch binding { case saml.HTTPRedirectBinding: if redirectURL, err := authReq.Redirect("", &serviceProvider); err != nil { log.Errorf("[SAML] Failed to format a redirect for SAML provider %s: %v", serviceProvider.EntityID, err) // Likely a technical or configuration issue - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else { response.Header().Add(headers.Location.String(), redirectURL.String()) response.WriteHeader(http.StatusFound) @@ -421,13 +421,13 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque if _, err := response.Write([]byte(fmt.Sprintf(authInitiationContentBodyFormat, authReq.Post("")))); err != nil { log.Errorf("[SAML] Failed to write response with HTTP POST binding: %v", err) // Technical issues scenario - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } default: log.Errorf("[SAML] Unhandled binding type %s", binding) // Treating unknown binding as a misconfiguration - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } } } @@ -437,15 +437,15 @@ func (s ManagementResource) SAMLLoginHandler(response http.ResponseWriter, reque func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, request *http.Request, ssoProvider model.SSOProvider) { if ssoProvider.SAMLProvider == nil { // SAML misconfiguration - redirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO Connection failed, please contact your Administrator") } else if serviceProvider, err := auth.NewServiceProvider(*ctx.Get(request.Context()).Host, s.config, *ssoProvider.SAMLProvider); err != nil { log.Errorf("[SAML] Service provider creation failed: %v", err) - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if err := request.ParseForm(); err != nil { log.Errorf("[SAML] Failed to parse form POST: %v", err) // Technical issues or invalid form data // This is not covered by acceptance criteria directly; treat as technical issue - redirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") + v2.RedirectToLoginPage(response, request, "We’re having trouble connecting. Please check your internet and try again.") } else if assertion, err := serviceProvider.ParseResponse(request, nil); err != nil { var typedErr *saml.InvalidResponseError switch { @@ -455,11 +455,11 @@ func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, re log.Errorf("[SAML] Failed to parse ACS response for provider %s: %v", ssoProvider.SAMLProvider.IssuerURI, err) } // SAML credentials issue scenario (authentication failed) - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else if principalName, err := ssoProvider.SAMLProvider.GetSAMLUserPrincipalNameFromAssertion(assertion); err != nil { log.Errorf("[SAML] Failed to lookup user for SAML provider %s: %v", ssoProvider.Name, err) // SAML credentials issue scenario again - redirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") + v2.RedirectToLoginPage(response, request, "Your SSO was unable to authenticate your user, please contact your Administrator") } else { if ssoProvider.Config.AutoProvision.Enabled { if err := jitSAMLUserCreation(request.Context(), ssoProvider, principalName, assertion, s.db); err != nil { @@ -473,15 +473,17 @@ func (s ManagementResource) SAMLCallbackHandler(response http.ResponseWriter, re } func jitSAMLUserCreation(ctx context.Context, ssoProvider model.SSOProvider, principalName string, assertion *saml.Assertion, u jitUserCreator) error { - if role, err := u.GetRole(ctx, ssoProvider.Config.AutoProvision.DefaultRoleId); err != nil { - return fmt.Errorf("get role: %v", err) + if roles, err := SanitizeAndGetRoles(ctx, ssoProvider.Config.AutoProvision, ssoProvider.SAMLProvider.GetSAMLUserRolesFromAssertion(assertion), u); err != nil { + return fmt.Errorf("sanitize roles: %v", err) + } else if len(roles) != 1 { + return fmt.Errorf("invalid roles detected") } else if _, err := u.LookupUser(ctx, principalName); err != nil && !errors.Is(err, database.ErrNotFound) { return fmt.Errorf("lookup user: %v", err) } else if errors.Is(err, database.ErrNotFound) { user := model.User{ EmailAddress: null.StringFrom(principalName), PrincipalName: principalName, - Roles: model.Roles{role}, + Roles: roles, SSOProviderID: null.Int32From(ssoProvider.ID), EULAAccepted: true, // EULA Acceptance does not pertain to Bloodhound Community Edition; this flag is used for Bloodhound Enterprise users FirstName: null.StringFrom(principalName), diff --git a/cmd/api/src/api/v2/auth/sso.go b/cmd/api/src/api/v2/auth/sso.go index 27d49aa6fe..643e03274e 100644 --- a/cmd/api/src/api/v2/auth/sso.go +++ b/cmd/api/src/api/v2/auth/sso.go @@ -18,15 +18,16 @@ package auth import ( "context" + "fmt" "net/http" "net/url" "path" "strconv" "strings" - "github.com/specterops/bloodhound/headers" - "github.com/gorilla/mux" + "github.com/specterops/bloodhound/dawgs/cardinality" + "github.com/specterops/bloodhound/log" "github.com/specterops/bloodhound/src/api" "github.com/specterops/bloodhound/src/auth" "github.com/specterops/bloodhound/src/ctx" @@ -61,8 +62,12 @@ type getRoler interface { GetRole(ctx context.Context, roleID int32) (model.Role, error) } +type getAllRoler interface { + GetAllRoles(ctx context.Context, order string, filter model.SQLFilter) (model.Roles, error) +} + type jitUserCreator interface { - getRoler + getAllRoler LookupUser(ctx context.Context, principalNameOrEmail string) (model.User, error) CreateUser(ctx context.Context, user model.User) (model.User, error) @@ -245,16 +250,45 @@ func (s ManagementResource) SSOCallbackHandler(response http.ResponseWriter, req } } -func redirectToLoginPage(response http.ResponseWriter, request *http.Request, errorMessage string) { - hostURL := *ctx.FromRequest(request).Host - redirectURL := api.URLJoinPath(hostURL, api.UserInterfacePath) +func SanitizeAndGetRoles(ctx context.Context, autoProvisionConfig model.SSOProviderAutoProvisionConfig, maybeBHRoles []string, r getAllRoler) (model.Roles, error) { + if dbRoles, err := r.GetAllRoles(ctx, "", model.SQLFilter{}); err != nil { + return nil, err + } else { + var defaultRole model.Role + dbRolesBySlug := make(map[string]*model.Role) + // Make quick lookup by role slug -> lower cased, dashes for spaces, and prefixed by `bh` e.g. bh-power-user + for _, r := range dbRoles { + dbRolesBySlug[fmt.Sprintf("bh-%s", strings.ReplaceAll(strings.ToLower(r.Name), " ", "-"))] = &r + if r.ID == autoProvisionConfig.DefaultRoleId { + defaultRole = r + } + } - // Optionally, include the error message as a query parameter or in session storage - query := redirectURL.Query() - query.Set("error", errorMessage) - redirectURL.RawQuery = query.Encode() + if autoProvisionConfig.RoleProvision { + var validRoles model.Roles + validRolesSeen := cardinality.NewBitmap32() // Ensure no dupes + // Only add valid roles + for _, r := range maybeBHRoles { + if dbRole := dbRolesBySlug[strings.ReplaceAll(strings.ToLower(r), " ", "-")]; dbRole != nil && !validRolesSeen.Contains(uint32(dbRole.ID)) { + validRoles = append(validRoles, *dbRole) + validRolesSeen.Add(uint32(dbRole.ID)) + } + } + switch { + case len(validRoles) == 1: + return validRoles, nil + case len(validRoles) > 1: + log.Warnf("[SSO] JIT Role Provision detected multiple valid roles - %s , falling back to default role %s", validRoles.Names(), defaultRole.Name) + default: + log.Warnf("[SSO] JIT Role Provision detected no valid roles from %s , falling back to default role %s", maybeBHRoles, defaultRole.Name) + } + } - // Redirect to the login page - response.Header().Add(headers.Location.String(), redirectURL.String()) - response.WriteHeader(http.StatusFound) + /* Fallback to default role: + - Role provision is disabled + - Role provision is enabled but no valid roles are found + - Role provision is enabled but multiple valid roles are found + */ + return model.Roles{defaultRole}, nil + } } diff --git a/cmd/api/src/api/v2/auth/sso_test.go b/cmd/api/src/api/v2/auth/sso_test.go index 7d97ade078..a9f1f4baaa 100644 --- a/cmd/api/src/api/v2/auth/sso_test.go +++ b/cmd/api/src/api/v2/auth/sso_test.go @@ -17,6 +17,7 @@ package auth_test import ( + "context" "net/http" "net/url" "testing" @@ -31,6 +32,7 @@ import ( "github.com/specterops/bloodhound/src/database/types/null" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/utils/test" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" ) @@ -247,3 +249,50 @@ func TestManagementResource_DeleteOIDCProvider(t *testing.T) { ResponseStatusCode(http.StatusNotFound) }) } + +func TestManagementResource_SanitizeAndGetRoles(t *testing.T) { + var ( + mockCtrl = gomock.NewController(t) + _, mockDB = apitest.NewAuthManagementResource(mockCtrl) + testCtx = context.Background() + + dbRoles = model.Roles{ + {Name: "God Role", Serial: model.Serial{ID: 1}}, + {Name: "Default Role", Serial: model.Serial{ID: 2}}, + {Name: "Valid Role", Serial: model.Serial{ID: 3}}, + } + roleProvisionEnabledConfig = model.SSOProviderAutoProvisionConfig{RoleProvision: true, DefaultRoleId: 2, Enabled: true} + roleProvisionDisabledConfig = model.SSOProviderAutoProvisionConfig{RoleProvision: false, DefaultRoleId: 2, Enabled: true} + ) + t.Run("role provision enabled - return valid role", func(t *testing.T) { + mockDB.EXPECT().GetAllRoles(gomock.Any(), "", model.SQLFilter{}).Return(dbRoles, nil) + roles, err := auth.SanitizeAndGetRoles(testCtx, roleProvisionEnabledConfig, []string{"ignored", "bh-valid-role"}, mockDB) + require.Nil(t, err) + require.Len(t, roles, 1) + require.Equal(t, roles[0].ID, dbRoles[2].ID) + }) + + t.Run("role provision enabled - return default role when multiple valid roles", func(t *testing.T) { + mockDB.EXPECT().GetAllRoles(gomock.Any(), "", model.SQLFilter{}).Return(dbRoles, nil) + roles, err := auth.SanitizeAndGetRoles(testCtx, roleProvisionEnabledConfig, []string{"bh-valid-role", "ignored", "bh-god-role"}, mockDB) + require.Nil(t, err) + require.Len(t, roles, 1) + require.Equal(t, roles[0].ID, roleProvisionEnabledConfig.DefaultRoleId) + }) + + t.Run("role provision enabled - return default role when no valid roles", func(t *testing.T) { + mockDB.EXPECT().GetAllRoles(gomock.Any(), "", model.SQLFilter{}).Return(dbRoles, nil) + roles, err := auth.SanitizeAndGetRoles(testCtx, roleProvisionEnabledConfig, []string{"bh-invalid-role", "ignored"}, mockDB) + require.Nil(t, err) + require.Len(t, roles, 1) + require.Equal(t, roles[0].ID, roleProvisionEnabledConfig.DefaultRoleId) + }) + + t.Run("role provision disabled - return default role", func(t *testing.T) { + mockDB.EXPECT().GetAllRoles(gomock.Any(), "", model.SQLFilter{}).Return(dbRoles, nil) + roles, err := auth.SanitizeAndGetRoles(testCtx, roleProvisionDisabledConfig, []string{"bh-valid-role", "ignored", "bh-god-role"}, mockDB) + require.Nil(t, err) + require.Len(t, roles, 1) + require.Equal(t, roles[0].ID, roleProvisionEnabledConfig.DefaultRoleId) + }) +} diff --git a/cmd/api/src/api/v2/helpers.go b/cmd/api/src/api/v2/helpers.go index 44c5fced30..5e73403418 100644 --- a/cmd/api/src/api/v2/helpers.go +++ b/cmd/api/src/api/v2/helpers.go @@ -26,7 +26,9 @@ import ( "time" "github.com/gorilla/mux" + "github.com/specterops/bloodhound/headers" "github.com/specterops/bloodhound/src/api" + "github.com/specterops/bloodhound/src/ctx" "github.com/specterops/bloodhound/src/model" "github.com/specterops/bloodhound/src/utils" ) @@ -37,6 +39,20 @@ func ErrBadQueryParameter(request *http.Request, key string, err error) *api.Err return api.BuildErrorResponse(http.StatusBadRequest, fmt.Sprintf("query parameter \"%s\" is malformed: %v", key, err), request) } +func RedirectToLoginPage(response http.ResponseWriter, request *http.Request, errorMessage string) { + hostURL := *ctx.FromRequest(request).Host + redirectURL := api.URLJoinPath(hostURL, api.UserInterfacePath) + + // Optionally, include the error message as a query parameter or in session storage + query := redirectURL.Query() + query.Set("error", errorMessage) + redirectURL.RawQuery = query.Encode() + + // Redirect to the login page + response.Header().Add(headers.Location.String(), redirectURL.String()) + response.WriteHeader(http.StatusFound) +} + func ParseIntQueryParameter(params url.Values, key string, defaultValue int) (int, error) { if param := params.Get(key); param != "" { return strconv.Atoi(param) diff --git a/cmd/api/src/model/auth.go b/cmd/api/src/model/auth.go index b86b49328b..7170050036 100644 --- a/cmd/api/src/model/auth.go +++ b/cmd/api/src/model/auth.go @@ -345,6 +345,16 @@ func (s Roles) GetValidFilterPredicatesAsStrings(column string) ([]string, error } } +func (s Roles) Names() []string { + names := make([]string, len(s)) + + for idx, role := range s { + names[idx] = role.Name + } + + return names +} + func (s Roles) IDs() []int32 { ids := make([]int32, len(s)) diff --git a/cmd/api/src/model/samlprovider.go b/cmd/api/src/model/samlprovider.go index 7d97427139..a851d140a0 100644 --- a/cmd/api/src/model/samlprovider.go +++ b/cmd/api/src/model/samlprovider.go @@ -39,6 +39,7 @@ const ( XMLSOAPClaimsGivenName = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname" XMLSOAPClaimsName = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" XMLSOAPClaimsSurname = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname" + MicrosoftClaimsRole = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role" ) var ( @@ -118,6 +119,11 @@ func (s SAMLProvider) givenNameAttributeNames() []string { return []string{ObjectIDGivenName, XMLSOAPClaimsGivenName, ObjectIDName, XMLSOAPClaimsName} } +func (s SAMLProvider) roleAttributeNames() []string { + // Added the MicrosoftClaimsRole as a fallback + return []string{MicrosoftClaimsRole} +} + func (s SAMLProvider) surnameAttributeNames() []string { return []string{ObjectIDSurname, XMLSOAPClaimsSurname} } @@ -165,6 +171,23 @@ func (s SAMLProvider) GetSAMLUserGivenNameFromAssertion(assertion *saml.Assertio return assertionFindString(assertion, s.givenNameAttributeNames()...) } +// GetSAMLUserRolesFromAssertion May be empty if not present +func (s SAMLProvider) GetSAMLUserRolesFromAssertion(assertion *saml.Assertion) (roles []string) { + for _, attributeStatement := range assertion.AttributeStatements { + for _, attribute := range attributeStatement.Attributes { + for _, validName := range s.roleAttributeNames() { + if attribute.Name == validName && len(attribute.Values) > 0 { + for _, value := range attribute.Values { + roles = append(roles, value.Value) + } + } + } + } + } + + return roles +} + func (s SAMLProvider) GetSAMLUserSurnameFromAssertion(assertion *saml.Assertion) (string, error) { return assertionFindString(assertion, s.surnameAttributeNames()...) } From ec2580aba8a12776126678c2265d85e2515f0bac Mon Sep 17 00:00:00 2001 From: mistahj67 <26472282+mistahj67@users.noreply.github.com> Date: Thu, 2 Jan 2025 10:40:21 -1000 Subject: [PATCH 8/9] BED-5069 feat: Add sso config options to providers (#990) --- .../CreateUserDialog.test.tsx | 46 +---- .../LoginViaSSOForm/LoginViaSSOForm.test.tsx | 29 +-- .../SSOProviderInfoPanel.test.tsx | 22 ++- .../SSOProviderInfoPanel.tsx | 50 +++-- .../SSOProviderTable.test.tsx | 4 +- .../UpdateUserDialog.test.tsx | 70 +------ .../UpdateUserForm/UpdateUserForm.tsx | 10 +- .../src/components/UpsertOIDCProviderForm.tsx | 2 +- .../SSOProviderConfigForm.tsx | 153 +++++++++++++++ .../UpsertOIDCProviderDialog.tsx | 42 ++--- .../UpsertOIDCProviderForm.tsx | 175 ++++++++++++++++++ .../UpsertSAMLProviderDialog.tsx | 6 +- .../UpsertSAMLProviderDialog/index.ts | 0 .../UpsertSAMLProviderForm.test.tsx | 22 ++- .../UpsertSAMLProviderForm.tsx | 26 ++- .../UpsertSAMLProviderForm/index.ts | 0 .../components/UpsertSSOProviders/index.ts | 19 ++ .../bh-shared-ui/src/components/index.ts | 8 +- .../bh-shared-ui/src/setupTests.tsx | 8 + .../SSOConfiguration.test.tsx | 28 ++- .../SSOConfiguration/SSOConfiguration.tsx | 28 ++- .../src/views/Users/Users.test.tsx | 19 +- .../bh-shared-ui/src/views/Users/Users.tsx | 1 - .../js-client-library/src/client.ts | 25 ++- .../javascript/js-client-library/src/types.ts | 25 ++- 25 files changed, 605 insertions(+), 213 deletions(-) create mode 100644 packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/SSOProviderConfigForm.tsx rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertOIDCProviderDialog.tsx (59%) create mode 100644 packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderForm.tsx rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx (88%) rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertSAMLProviderDialog/index.ts (100%) rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx (80%) rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx (88%) rename packages/javascript/bh-shared-ui/src/components/{ => UpsertSSOProviders}/UpsertSAMLProviderForm/index.ts (100%) create mode 100644 packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/index.ts diff --git a/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx b/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx index f13e0ea360..c2ebfc64d1 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/CreateUserDialog/CreateUserDialog.test.tsx @@ -19,7 +19,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { render, screen, waitFor } from '../../test-utils'; import CreateUserDialog from './CreateUserDialog'; -import { ListSSOProvidersResponse, SAMLProviderInfo, SSOProvider } from 'js-client-library'; +import { ListSSOProvidersResponse, SAMLProviderInfo, SSOProvider, SSOProviderConfiguration } from 'js-client-library'; const testRoles = [ { id: 1, name: 'Role 1' }, @@ -34,76 +34,48 @@ const testSSOProviders: SSOProvider[] = [ name: 'saml-provider-1', slug: 'saml-provider-1', type: 'SAML', - details: { - idp_issuer_uri: '', - idp_sso_uri: '', - principal_attribute_mappings: null, - sp_issuer_uri: '', - sp_sso_uri: '', - sp_metadata_uri: '', - sp_acs_uri: '', - } as SAMLProviderInfo, login_uri: '', callback_uri: '', created_at: '', updated_at: '', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { id: 2, name: 'saml-provider-2', slug: 'saml-provider-2', type: 'SAML', - details: { - idp_issuer_uri: '', - idp_sso_uri: '', - principal_attribute_mappings: null, - sp_issuer_uri: '', - sp_sso_uri: '', - sp_metadata_uri: '', - sp_acs_uri: '', - } as SAMLProviderInfo, login_uri: '', callback_uri: '', created_at: '', updated_at: '', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { id: 3, name: 'saml-provider-3', slug: 'saml-provider-3', type: 'SAML', - details: { - idp_issuer_uri: '', - idp_sso_uri: '', - principal_attribute_mappings: null, - sp_issuer_uri: '', - sp_sso_uri: '', - sp_metadata_uri: '', - sp_acs_uri: '', - } as SAMLProviderInfo, login_uri: '', callback_uri: '', created_at: '', updated_at: '', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { id: 4, name: 'saml-provider-4', slug: 'saml-provider-4', type: 'SAML', - details: { - idp_issuer_uri: '', - idp_sso_uri: '', - principal_attribute_mappings: null, - sp_issuer_uri: '', - sp_sso_uri: '', - sp_metadata_uri: '', - sp_acs_uri: '', - } as SAMLProviderInfo, login_uri: '', callback_uri: '', created_at: '', updated_at: '', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, ]; diff --git a/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx b/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx index 292742ccbf..1afcc9f651 100644 --- a/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/LoginViaSSOForm/LoginViaSSOForm.test.tsx @@ -17,51 +17,32 @@ import userEvent from '@testing-library/user-event'; import { act, render, screen } from '../../test-utils'; import LoginViaSSOForm from './LoginViaSSOForm'; -import { SSOProvider } from 'js-client-library'; +import { OIDCProviderInfo, SAMLProviderInfo, SSOProvider, SSOProviderConfiguration } from 'js-client-library'; const testSSOProviders: SSOProvider[] = [ { name: 'sso-provider-1', slug: 'test-slug-1', type: 'OIDC', - details: { - client_id: '', - issuer: '', - sso_provider_id: 2, - id: 1, - created_at: '', - updated_at: '', - }, login_uri: '', callback_uri: '', id: 1, created_at: '', updated_at: '', + details: {} as OIDCProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { name: 'sso-provider-2', slug: 'test-slug-2', type: 'SAML', - details: { - name: '', - display_name: '', - idp_issuer_uri: '', - idp_sso_uri: '', - principal_attribute_mappings: null, - sp_issuer_uri: '', - sp_metadata_uri: '', - sp_acs_uri: '', - sp_sso_uri: '', - sso_provider_id: 1, - id: 1, - created_at: '', - updated_at: '', - }, login_uri: '', callback_uri: '', id: 2, created_at: '', updated_at: '', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, ]; diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx index 0f09c29609..21df26d972 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.test.tsx @@ -37,6 +37,9 @@ const samlProvider: SSOProvider = { callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', + config: { + auto_provision: { enabled: false, role_provision: false, default_role_id: 0 }, + }, }; const oidcProvider: SSOProvider = { @@ -49,9 +52,12 @@ const oidcProvider: SSOProvider = { client_id: 'gotham-oidc', } as OIDCProviderInfo, login_uri: '', - callback_uri: '', + callback_uri: 'http://bloodhound.localhost/api/v2/sso/test-idp-2/callback', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', + config: { + auto_provision: { enabled: true, role_provision: true, default_role_id: 1 }, + }, }; describe('SSOProviderTable', () => { @@ -64,6 +70,15 @@ describe('SSOProviderTable', () => { expect(await screen.findByText(samlInfo.sp_sso_uri)).toBeInTheDocument(); expect(await screen.findByText(samlInfo.sp_acs_uri)).toBeInTheDocument(); expect(await screen.findByText(samlInfo.sp_metadata_uri)).toBeInTheDocument(); + + expect(await screen.findByText('Automatically create new users on login')).toBeInTheDocument(); + // This provider has IDP provisioning disabled which should hide these 2 fields + expect(screen.queryByText('Allow SSO provider to manage roles for new users')).not.toBeInTheDocument(); + expect(screen.queryByText('Default role when creating new users')).not.toBeInTheDocument(); + + expect( + screen.getByRole('button', { name: `Download ${samlProvider.name} SP Certificate` }) + ).toBeInTheDocument(); }); it('should render oidc info provider', async () => { @@ -73,5 +88,10 @@ describe('SSOProviderTable', () => { expect(await screen.findByText(oidcInfo.issuer)).toBeInTheDocument(); expect(await screen.findByText(oidcInfo.client_id)).toBeInTheDocument(); + expect(await screen.findByText(oidcProvider.callback_uri)).toBeInTheDocument(); + + expect(await screen.findByText('Automatically create new users on login')).toBeInTheDocument(); + expect(await screen.findByText('Allow SSO provider to manage roles for new users')).toBeInTheDocument(); + expect(await screen.findByText('Default role when creating new users')).toBeInTheDocument(); }); }); diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.tsx index 081c72750d..08da9b91d5 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderInfoPanel/SSOProviderInfoPanel.tsx @@ -15,10 +15,10 @@ // SPDX-License-Identifier: Apache-2.0 import { Paper, Box, Typography, useTheme } from '@mui/material'; -import { FC } from 'react'; +import { FC, useMemo } from 'react'; import fileDownload from 'js-file-download'; -import { OIDCProviderInfo, SAMLProviderInfo, SSOProvider } from 'js-client-library'; -import { Button } from '@bloodhoundenterprise/doodleui'; +import { OIDCProviderInfo, SAMLProviderInfo, SSOProvider, Role } from 'js-client-library'; +import { Button, Label } from '@bloodhoundenterprise/doodleui'; import { Field, FieldsContainer, usePaneStyles, useHeaderStyles } from '../../views/Explore'; import LabelWithCopy from '../LabelWithCopy'; import { apiClient } from '../../utils'; @@ -27,7 +27,7 @@ import { useNotifications } from '../../providers'; const SAMLProviderInfoPanel: FC<{ samlProviderDetails: SAMLProviderInfo; }> = ({ samlProviderDetails }) => ( - + <> } value={samlProviderDetails.idp_sso_uri} @@ -46,7 +46,7 @@ const SAMLProviderInfoPanel: FC<{ } value={samlProviderDetails.sp_metadata_uri} /> - + ); const OIDCProviderInfoPanel: FC<{ @@ -54,7 +54,7 @@ const OIDCProviderInfoPanel: FC<{ }> = ({ ssoProvider }) => { const oidcProviderDetails = ssoProvider.details as OIDCProviderInfo; return ( - + <> } value={oidcProviderDetails.client_id} @@ -67,32 +67,38 @@ const OIDCProviderInfoPanel: FC<{ label={} value={ssoProvider.callback_uri} /> - + ); }; const SSOProviderInfoPanel: FC<{ ssoProvider: SSOProvider; -}> = ({ ssoProvider }) => { + roles?: Role[]; +}> = ({ ssoProvider, roles }) => { const theme = useTheme(); const paneStyles = usePaneStyles(); const headerStyles = useHeaderStyles(); const { addNotification } = useNotifications(); + const defaultRoleName = useMemo( + () => roles?.find((role) => role.id === ssoProvider.config?.auto_provision?.default_role_id)?.name, + [roles, ssoProvider.config?.auto_provision?.default_role_id] + ); + if (!ssoProvider.type) { return null; } - let infoPanel; + let innerInfoPanel; switch (ssoProvider.type.toLowerCase()) { case 'saml': - infoPanel = ; + innerInfoPanel = ; break; case 'oidc': - infoPanel = ; + innerInfoPanel = ; break; default: - infoPanel = null; + innerInfoPanel = null; } const downloadSAMLSigningCertificate = () => { @@ -157,7 +163,25 @@ const SSOProviderInfoPanel: FC<{ Provider Information: - {infoPanel} + + {innerInfoPanel} + Automatically create new users on login} + value={ssoProvider.config?.auto_provision?.enabled ? 'Yes' : 'No'} + /> + {ssoProvider.config?.auto_provision?.enabled && ( + <> + Allow SSO provider to manage roles for new users} + value={ssoProvider.config?.auto_provision?.role_provision ? 'Yes' : 'No'} + /> + Default role when creating new users} + value={defaultRoleName ?? 'Read-Only'} + /> + + )} + diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx index 6997e7c343..0f3672992b 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.test.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import userEvent from '@testing-library/user-event'; -import { OIDCProviderInfo, SAMLProviderInfo, SSOProvider } from 'js-client-library'; +import { OIDCProviderInfo, SAMLProviderInfo, SSOProvider, SSOProviderConfiguration } from 'js-client-library'; import { render, screen } from '../../test-utils'; import { SortOrder } from '../../utils'; import SSOProviderTable from './SSOProviderTable'; @@ -30,6 +30,7 @@ const samlProvider: SSOProvider = { created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }; const oidcProvider: SSOProvider = { @@ -42,6 +43,7 @@ const oidcProvider: SSOProvider = { created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', details: {} as OIDCProviderInfo, + config: {} as SSOProviderConfiguration['config'], }; const ssoProviders = [samlProvider, oidcProvider]; diff --git a/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx b/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx index dd7a03e515..0cd0fbef37 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpdateUserDialog/UpdateUserDialog.test.tsx @@ -19,7 +19,7 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { render, screen, waitFor } from '../../test-utils'; import UpdateUserDialog from './UpdateUserDialog'; -import { ListSSOProvidersResponse, SSOProvider } from 'js-client-library'; +import { ListSSOProvidersResponse, SAMLProviderInfo, SSOProvider, SSOProviderConfiguration } from 'js-client-library'; const testRoles = [ { id: 1, name: 'Role 1' }, @@ -33,101 +33,49 @@ const testSSOProviders: SSOProvider[] = [ name: 'saml-provider-1', slug: 'saml-provider-1', type: 'SAML', - details: { - name: 'saml-provider', - display_name: 'saml-provider', - idp_issuer_uri: 'urn:saml-provider.com', - idp_sso_uri: 'https://saml-provider.com/saml', - principal_attribute_mappings: null, - sp_issuer_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider', - sp_sso_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/sso', - sp_metadata_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/metadata', - sp_acs_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/acs', - sso_provider_id: 1, - id: 1, - created_at: '2024-01-01T12:00:00Z', - updated_at: '2024-01-01T12:00:00Z', - }, login_uri: '', callback_uri: '', id: 1, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { name: 'saml-provider-2', slug: 'saml-provider-2', type: 'SAML', - details: { - name: 'saml-provider', - display_name: 'saml-provider', - idp_issuer_uri: 'urn:saml-provider.com', - idp_sso_uri: 'https://saml-provider.com/saml', - principal_attribute_mappings: null, - sp_issuer_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider', - sp_sso_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/sso', - sp_metadata_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/metadata', - sp_acs_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/acs', - sso_provider_id: 1, - id: 1, - created_at: '2024-01-01T12:00:00Z', - updated_at: '2024-01-01T12:00:00Z', - }, login_uri: '', callback_uri: '', id: 2, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { name: 'saml-provider-3', slug: 'saml-provider-3', type: 'SAML', - details: { - name: 'saml-provider', - display_name: 'saml-provider', - idp_issuer_uri: 'urn:saml-provider.com', - idp_sso_uri: 'https://saml-provider.com/saml', - principal_attribute_mappings: null, - sp_issuer_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider', - sp_sso_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/sso', - sp_metadata_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/metadata', - sp_acs_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/acs', - sso_provider_id: 1, - id: 1, - created_at: '2024-01-01T12:00:00Z', - updated_at: '2024-01-01T12:00:00Z', - }, login_uri: '', callback_uri: '', id: 3, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, { name: 'saml-provider-4', slug: 'saml-provider-4', type: 'SAML', - details: { - name: 'saml-provider', - display_name: 'saml-provider', - idp_issuer_uri: 'urn:saml-provider.com', - idp_sso_uri: 'https://saml-provider.com/saml', - principal_attribute_mappings: null, - sp_issuer_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider', - sp_sso_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/sso', - sp_metadata_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/metadata', - sp_acs_uri: 'https://test.bloodhoundenterprise.io/api/v1/login/saml/saml-provider/acs', - sso_provider_id: 1, - id: 1, - created_at: '2024-01-01T12:00:00Z', - updated_at: '2024-01-01T12:00:00Z', - }, login_uri: '', callback_uri: '', id: 4, created_at: '2024-01-01T12:00:00Z', updated_at: '2024-01-01T12:00:00Z', + details: {} as SAMLProviderInfo, + config: {} as SSOProviderConfiguration['config'], }, ]; diff --git a/packages/javascript/bh-shared-ui/src/components/UpdateUserForm/UpdateUserForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpdateUserForm/UpdateUserForm.tsx index 3c8c4c7a32..bba633c20b 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpdateUserForm/UpdateUserForm.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpdateUserForm/UpdateUserForm.tsx @@ -33,7 +33,7 @@ import React, { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { useQuery } from 'react-query'; import { apiClient } from '../../utils'; -import { SSOProvider, UpdateUserRequest } from 'js-client-library'; +import { SSOProvider, UpdateUserRequest, Role } from 'js-client-library'; export type UpdateUserRequestForm = Omit & { SSOProviderId: string | undefined }; @@ -130,7 +130,7 @@ const UpdateUserFormInner: React.FC<{ onCancel: () => void; onSubmit: (user: UpdateUserRequestForm) => void; initialData: UpdateUserRequestForm; - roles: any[]; + roles?: Role[]; SSOProviders?: SSOProvider[]; hasSelectedSelf: boolean; isLoading: boolean; @@ -363,9 +363,9 @@ const UpdateUserFormInner: React.FC<{ value={isNaN(field.value) ? '' : field.value.toString()} variant='standard' fullWidth - data-testid='update-user-dialog_select-role' - hidden={hasSelectedSelf}> - {roles.map((role: any) => ( + data-testid='update-user-dialog_select-role'> + hidden={hasSelectedSelf} + {roles?.map((role: Role) => ( {role.name} diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx index 6048c91134..8cf28614af 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx @@ -21,7 +21,7 @@ import { Controller, useForm } from 'react-hook-form'; import { OIDCProviderInfo, SSOProvider, UpsertOIDCProviderRequest } from 'js-client-library'; const UpsertOIDCProviderForm: FC<{ - error: any; + error?: any; oldSSOProvider?: SSOProvider; onClose: () => void; onSubmit: (data: UpsertOIDCProviderRequest) => void; diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/SSOProviderConfigForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/SSOProviderConfigForm.tsx new file mode 100644 index 0000000000..d02e8f57bb --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/SSOProviderConfigForm.tsx @@ -0,0 +1,153 @@ +// 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 + +import { Switch } from '@bloodhoundenterprise/doodleui'; +import { + Alert, + Grid, + Typography, + FormControl, + InputLabel, + Select, + MenuItem, + FormControlLabel, + useTheme, +} from '@mui/material'; +import { FC } from 'react'; +import { Control, Controller, FieldErrors, UseFormResetField, UseFormWatch } from 'react-hook-form'; +import { Role, UpsertOIDCProviderRequest, UpsertSAMLProviderFormInputs } from 'js-client-library'; + +export const backfillSSOProviderConfig = (readOnlyRoleId?: number) => ({ + auto_provision: { enabled: false, default_role: readOnlyRoleId, role_provision: false }, +}); + +const SSOProviderConfigForm: FC<{ + control: Control; + errors: FieldErrors; + watch: UseFormWatch; + resetField: UseFormResetField; + roles?: Role[]; + readOnlyRoleId?: number; +}> = ({ control, errors, readOnlyRoleId, resetField, roles, watch }) => { + const theme = useTheme(); + + return ( + <> + + ( + { + field.onChange(checked); + if (!checked) { + resetField('config.auto_provision.role_provision'); + resetField('config.auto_provision.default_role_id'); + } + }} + color='primary' + data-testid='sso-provider-config-form_toggle-auto-provision' + /> + } + label={ + + Automatically create new users on login + + } + /> + )} + /> + + + ( + field.onChange(checked)} + color='primary' + data-testid='sso-provider-config-form_toggle-role-provision' + /> + } + label={ + + Allow SSO Provider to modify roles + + } + /> + )} + /> + + + value != 0 || 'Default role is required', + }} + render={({ field }) => ( + + + Default User Role + + + + )} + /> + + {!!errors.config?.auto_provision?.default_role_id && ( + + {errors.config?.auto_provision?.default_role_id?.message} + + )} + + ); +}; + +export default SSOProviderConfigForm; diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderDialog.tsx similarity index 59% rename from packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderDialog.tsx index 11ab94b40f..591fe74cb7 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderDialog.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Dialog, DialogTitle } from '@mui/material'; -import { SSOProvider, UpsertOIDCProviderRequest } from 'js-client-library'; +import { SSOProvider, UpsertOIDCProviderRequest, Role } from 'js-client-library'; import UpsertOIDCProviderForm from './UpsertOIDCProviderForm'; const UpsertOIDCProviderDialog: React.FC<{ @@ -24,26 +24,26 @@ const UpsertOIDCProviderDialog: React.FC<{ oldSSOProvider?: SSOProvider; onClose: () => void; onSubmit: (data: UpsertOIDCProviderRequest) => void; -}> = ({ open, error, oldSSOProvider, onClose, onSubmit }) => { - return ( -

= ({ open, error, oldSSOProvider, onClose, onSubmit, roles }) => ( + + {oldSSOProvider ? 'Edit' : 'Create'} OIDC Provider + - {oldSSOProvider ? 'Edit' : 'Create'} OIDC Provider - - - ); -}; + oldSSOProvider={oldSSOProvider} + onSubmit={onSubmit} + roles={roles} + /> + +); export default UpsertOIDCProviderDialog; diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderForm.tsx new file mode 100644 index 0000000000..aa3d0b5573 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertOIDCProviderForm.tsx @@ -0,0 +1,175 @@ +// 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 + +import { Button } from '@bloodhoundenterprise/doodleui'; +import { Alert, DialogContent, DialogActions, Grid, TextField } from '@mui/material'; +import { useEffect, FC } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { OIDCProviderInfo, SSOProvider, UpsertOIDCProviderRequest, Role } from 'js-client-library'; +import SSOProviderConfigForm, { backfillSSOProviderConfig } from './SSOProviderConfigForm'; + +const UpsertOIDCProviderForm: FC<{ + error: any; + oldSSOProvider?: SSOProvider; + roles?: Role[]; + onClose: () => void; + onSubmit: (data: UpsertOIDCProviderRequest) => void; +}> = ({ error, oldSSOProvider, roles, onClose, onSubmit }) => { + const readOnlyRoleId = roles?.find((role) => role.name === 'Read-Only')?.id; + + const defaultValues = { + name: oldSSOProvider?.name ?? '', + client_id: (oldSSOProvider?.details as OIDCProviderInfo)?.client_id ?? '', + issuer: (oldSSOProvider?.details as OIDCProviderInfo)?.issuer ?? '', + config: oldSSOProvider?.config ? oldSSOProvider.config : backfillSSOProviderConfig(readOnlyRoleId), + }; + + const { + control, + formState: { errors }, + handleSubmit, + reset, + resetField, + setError, + watch, + } = useForm({ defaultValues }); + + useEffect(() => { + if (error) { + if (error?.response?.status === 409) { + if (error.response?.data?.errors[0]?.message.toLowerCase().includes('sso provider name')) { + setError('name', { type: 'custom', message: 'SSO Provider Name is already in use.' }); + } else { + setError('root.generic', { + type: 'custom', + message: 'A conflict has occured.', + }); + } + } else { + setError('root.generic', { + type: 'custom', + message: `Unable to ${oldSSOProvider ? 'update' : 'create new'} OIDC Provider configuration. Please try again.`, + }); + } + } + }, [error, setError, oldSSOProvider]); + + const handleClose = () => { + onClose(); + reset(); + }; + + return ( +
+ + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + {!!errors.root?.generic && ( + + {errors.root.generic.message} + + )} + + + + + + +
+ ); +}; + +export default UpsertOIDCProviderForm; diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx similarity index 88% rename from packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx index 7885e67105..33bc9ea211 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Dialog, DialogTitle } from '@mui/material'; -import { SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; +import { Role, SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; import UpsertSAMLProviderForm from '../UpsertSAMLProviderForm'; const UpsertSAMLProviderDialog: React.FC<{ @@ -24,7 +24,8 @@ const UpsertSAMLProviderDialog: React.FC<{ oldSSOProvider?: SSOProvider; onClose: () => void; onSubmit: (data: UpsertSAMLProviderFormInputs) => void; -}> = ({ open, error, oldSSOProvider, onClose, onSubmit }) => { + roles?: Role[]; +}> = ({ open, error, oldSSOProvider, onClose, onSubmit, roles }) => { return ( ); diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/index.ts b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderDialog/index.ts similarity index 100% rename from packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/index.ts rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderDialog/index.ts diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx similarity index 80% rename from packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx index 31c6f8cc4b..d5c0287126 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx @@ -15,19 +15,31 @@ // SPDX-License-Identifier: Apache-2.0 import userEvent from '@testing-library/user-event'; -import { render, screen, waitFor } from '../../test-utils'; +import { render, screen, waitFor } from '../../../test-utils'; import UpsertSAMLProviderForm from './UpsertSAMLProviderForm'; +import { Role } from 'js-client-library'; + +const testRoles = [ + { id: 1, name: 'Read-Only' }, + { id: 2, name: 'Power User' }, + { id: 3, name: 'Administrator' }, + { id: 4, name: 'Upload Only' }, +] as Role[]; describe('UpsertSAMLProviderForm', () => { it('should render inputs, labels, and action buttons', () => { const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); expect(screen.getByLabelText('SAML Provider Name')).toBeInTheDocument(); expect(screen.getByLabelText('Choose File')).toBeInTheDocument(); + expect(screen.getByTestId('sso-provider-config-form_toggle-auto-provision')).toBeInTheDocument(); + expect(screen.getByTestId('sso-provider-config-form_toggle-role-provision')).toBeInTheDocument(); + expect(screen.getByTestId('sso-provider-config-form_select-default-role')).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Cancel' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Submit' })).toBeInTheDocument(); @@ -37,7 +49,7 @@ describe('UpsertSAMLProviderForm', () => { const user = userEvent.setup(); const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); await user.click(screen.getByRole('button', { name: 'Cancel' })); @@ -48,7 +60,7 @@ describe('UpsertSAMLProviderForm', () => { const user = userEvent.setup(); const testOnClose = vi.fn(); const testOnSubmit = vi.fn(); - render(); + render(); await user.click(screen.getByRole('button', { name: 'Submit' })); @@ -65,7 +77,7 @@ describe('UpsertSAMLProviderForm', () => { const testOnSubmit = vi.fn(); const validProviderName = 'test-provider-name'; const validMetadata = new File([], 'test-metadata.xml'); - render(); + render(); await user.type(screen.getByLabelText('SAML Provider Name'), validProviderName); diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx similarity index 88% rename from packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx index 5fb44001e5..38f6497311 100644 --- a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx @@ -26,27 +26,35 @@ import { Typography, useTheme, } from '@mui/material'; -import { useState, useEffect, FC } from 'react'; +import { useState, useEffect, FC, useMemo } from 'react'; import { Controller, useForm } from 'react-hook-form'; -import { SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; +import { Role, SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; +import SSOProviderConfigForm, { backfillSSOProviderConfig } from '../SSOProviderConfigForm'; const UpsertSAMLProviderForm: FC<{ - error: any; + error?: any; oldSSOProvider?: SSOProvider; onClose: () => void; onSubmit: (data: UpsertSAMLProviderFormInputs) => void; -}> = ({ error, onClose, oldSSOProvider, onSubmit }) => { + roles?: Role[]; +}> = ({ error, onClose, oldSSOProvider, onSubmit, roles }) => { const theme = useTheme(); + + const readOnlyRoleId = useMemo(() => roles?.find((role) => role.name === 'Read-Only')?.id, [roles]); + const { control, + formState: { errors }, handleSubmit, reset, - formState: { errors }, + resetField, setError, + watch, } = useForm({ defaultValues: { name: oldSSOProvider?.name ?? '', metadata: undefined, + config: oldSSOProvider?.config ? oldSSOProvider.config : backfillSSOProviderConfig(readOnlyRoleId), }, }); const [fileValue, setFileValue] = useState(''); // small workaround to use the file input @@ -147,6 +155,14 @@ const UpsertSAMLProviderForm: FC<{ : 'Upload the Metadata file provided by your SAML Provider'} + {!!errors.root?.generic && ( {errors.root.generic.message} diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/index.ts b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/index.ts similarity index 100% rename from packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/index.ts rename to packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/UpsertSAMLProviderForm/index.ts diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/index.ts b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/index.ts new file mode 100644 index 0000000000..308aab2b59 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSSOProviders/index.ts @@ -0,0 +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 + +export { default as UpsertSAMLProviderDialog } from './UpsertSAMLProviderDialog'; + +export { default as UpsertOIDCProviderDialog } from './UpsertOIDCProviderDialog'; diff --git a/packages/javascript/bh-shared-ui/src/components/index.ts b/packages/javascript/bh-shared-ui/src/components/index.ts index 5d6919bc43..415009bc5e 100644 --- a/packages/javascript/bh-shared-ui/src/components/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/index.ts @@ -170,11 +170,11 @@ export { default as UpdateUserDialog } from './UpdateUserDialog'; export * from './UpdateUserForm'; export { default as UpdateUserForm } from './UpdateUserForm'; -export * from './UpsertSAMLProviderDialog'; -export { default as UpsertSAMLProviderDialog } from './UpsertSAMLProviderDialog'; +export * from './UpsertSSOProviders/UpsertSAMLProviderDialog'; +export { default as UpsertSAMLProviderDialog } from './UpsertSSOProviders/UpsertSAMLProviderDialog'; -export * from './UpsertSAMLProviderForm'; -export { default as UpsertSAMLProviderForm } from './UpsertSAMLProviderForm'; +export * from './UpsertSSOProviders/UpsertSAMLProviderForm'; +export { default as UpsertSAMLProviderForm } from './UpsertSSOProviders/UpsertSAMLProviderForm'; export * from './UserTokenManagementDialog'; export { default as UserTokenManagementDialog } from './UserTokenManagementDialog'; diff --git a/packages/javascript/bh-shared-ui/src/setupTests.tsx b/packages/javascript/bh-shared-ui/src/setupTests.tsx index 0c5d115d9d..6e2c7e108a 100644 --- a/packages/javascript/bh-shared-ui/src/setupTests.tsx +++ b/packages/javascript/bh-shared-ui/src/setupTests.tsx @@ -39,3 +39,11 @@ vi.mock('@fortawesome/react-fontawesome', () => ({ return {props.icon.iconName}; }), })); + +const ResizeObserverMock = vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), +})); + +vi.stubGlobal('ResizeObserver', ResizeObserverMock); diff --git a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx index d6a13ebbbf..62b26ac189 100644 --- a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx +++ b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.test.tsx @@ -19,7 +19,14 @@ import { rest } from 'msw'; import { setupServer } from 'msw/node'; import { render, screen } from '../../test-utils'; import SSOConfiguration from './SSOConfiguration'; -import { ListSSOProvidersResponse, SAMLProviderInfo, SSOProvider } from 'js-client-library'; +import { ListRolesResponse, ListSSOProvidersResponse, Role, SAMLProviderInfo, SSOProvider } from 'js-client-library'; + +const testRoles = [ + { id: 1, name: 'Read-Only' }, + { id: 2, name: 'Power User' }, + { id: 3, name: 'Administrator' }, + { id: 4, name: 'Upload Only' }, +] as Role[]; const initialSAMLProvider: SSOProvider = { id: 1, @@ -39,6 +46,9 @@ const initialSAMLProvider: SSOProvider = { callback_uri: '', created_at: '2022-02-24T23:38:41.420271Z', updated_at: '2022-02-24T23:38:41.420271Z', + config: { + auto_provision: { enabled: false, role_provision: false, default_role_id: 1 }, + }, }; const ssoProviders = [initialSAMLProvider]; @@ -61,6 +71,9 @@ const newSAMLProvider: SSOProvider = { callback_uri: '', created_at: new Date().toISOString(), updated_at: new Date().toISOString(), + config: { + auto_provision: { enabled: false, role_provision: false, default_role_id: 1 }, + }, }; interface CreateSAMLProviderBody { @@ -84,6 +97,15 @@ interface CreateSAMLProviderResponse { } const server = setupServer( + rest.get(`/api/v2/roles`, (req, res, ctx) => { + return res( + ctx.json({ + data: { + roles: testRoles, + }, + }) + ); + }), rest.get('/api/v2/sso-providers', (req, res, ctx) => { return res( ctx.json({ @@ -114,7 +136,9 @@ beforeEach(() => { }; }); }); -beforeAll(() => server.listen()); +beforeAll(() => { + server.listen(); +}); afterEach(() => server.resetHandlers()); afterAll(() => server.close()); diff --git a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx index 1b60793575..65ed21d587 100644 --- a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx +++ b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx @@ -27,9 +27,8 @@ import { PageWithTitle, SSOProviderInfoPanel, SSOProviderTable, - UpsertSAMLProviderDialog, } from '../../components'; -import UpsertOIDCProviderDialog from '../../components/UpsertOIDCProviderDialog'; +import { UpsertOIDCProviderDialog, UpsertSAMLProviderDialog } from '../../components/UpsertSSOProviders'; import { useFeatureFlag } from '../../hooks'; import { useNotifications } from '../../providers'; import { SortOrder, apiClient } from '../../utils'; @@ -47,6 +46,10 @@ const SSOConfiguration: FC = () => { const [upsertProviderError, setUpsertProviderError] = useState(); const [typeSortOrder, setTypeSortOrder] = useState(); + const getRolesQuery = useQuery(['getRoles'], ({ signal }) => + apiClient.getRoles({ signal }).then((res) => res.data.data.roles) + ); + const listSSOProvidersQuery = useQuery(['listSSOProviders'], ({ signal }) => apiClient.listSSOProviders({ signal }).then((res) => res.data.data) ); @@ -173,12 +176,20 @@ const SSOConfiguration: FC = () => { const upsertSAMLProvider = async (samlProvider: UpsertSAMLProviderFormInputs) => { setUpsertProviderError(null); try { - const payload = { name: samlProvider.name, metadata: samlProvider.metadata && samlProvider.metadata[0] }; + const payload = { + name: samlProvider.name, + metadata: samlProvider.metadata && samlProvider.metadata[0], + config: samlProvider.config, + }; if (ssoProviderIdToDeleteOrUpdate) { await apiClient.updateSAMLProviderFromFile(ssoProviderIdToDeleteOrUpdate, payload); } else { - if (payload.name && payload.metadata) { - await apiClient.createSAMLProviderFromFile({ name: payload.name, metadata: payload.metadata }); + if (payload.name && payload.metadata && payload.config) { + await apiClient.createSAMLProviderFromFile({ + name: payload.name, + metadata: payload.metadata, + config: payload.config, + }); } } listSSOProvidersQuery.refetch(); @@ -195,11 +206,12 @@ const SSOConfiguration: FC = () => { if (ssoProviderIdToDeleteOrUpdate) { await apiClient.updateOIDCProvider(ssoProviderIdToDeleteOrUpdate, oidcProvider); } else { - if (oidcProvider.name && oidcProvider.client_id && oidcProvider.issuer) { + if (oidcProvider.name && oidcProvider.client_id && oidcProvider.issuer && oidcProvider.config) { await apiClient.createOIDCProvider({ name: oidcProvider.name, client_id: oidcProvider.client_id, issuer: oidcProvider.issuer, + config: oidcProvider.config, }); } } @@ -279,7 +291,7 @@ const SSOConfiguration: FC = () => { {selectedSSOProvider && ( - + )} @@ -290,6 +302,7 @@ const SSOConfiguration: FC = () => { error={upsertProviderError} onClose={closeDialog} onSubmit={upsertSAMLProvider} + roles={getRolesQuery.data} /> { error={upsertProviderError} onClose={closeDialog} onSubmit={upsertOIDCProvider} + roles={getRolesQuery.data} /> { const user = listUsersQuery.data?.find((user: User) => { return user.id === userId; }); - if (!user) { return; } diff --git a/packages/javascript/js-client-library/src/client.ts b/packages/javascript/js-client-library/src/client.ts index 313f8109b3..969e10ce02 100644 --- a/packages/javascript/js-client-library/src/client.ts +++ b/packages/javascript/js-client-library/src/client.ts @@ -640,16 +640,23 @@ class BHEAPIClient { logout = (options?: types.RequestOptions) => this.baseClient.post('/api/v2/logout', options); - createSAMLProviderFromFile = (data: { name: string; metadata: File }, options?: types.RequestOptions) => { + createSAMLProviderFromFile = ( + data: { name: string; metadata: File } & types.SSOProviderConfiguration, + options?: types.RequestOptions + ) => { + // form data is limited to strings or blobs so we have to deconstruct the config payload const formData = new FormData(); formData.append('name', data.name); formData.append('metadata', data.metadata); + formData.append('config.auto_provision.enabled', data.config.auto_provision.enabled.toString()); + formData.append('config.auto_provision.default_role_id', data.config.auto_provision.default_role_id.toString()); + formData.append('config.auto_provision.role_provision', data.config.auto_provision.role_provision.toString()); return this.baseClient.post(`/api/v2/sso-providers/saml`, formData, options); }; updateSAMLProviderFromFile = ( ssoProviderId: types.SSOProvider['id'], - data: { name?: string; metadata?: File }, + data: { name?: string; metadata?: File; config?: types.SSOProviderConfiguration['config'] }, options?: types.RequestOptions ) => { const formData = new FormData(); @@ -659,6 +666,17 @@ class BHEAPIClient { if (data.metadata) { formData.append('metadata', data.metadata); } + if (data.config) { + formData.append('config.auto_provision.enabled', data.config.auto_provision.enabled.toString()); + formData.append( + 'config.auto_provision.default_role_id', + data.config.auto_provision.default_role_id.toString() + ); + formData.append( + 'config.auto_provision.role_provision', + data.config.auto_provision.role_provision.toString() + ); + } return this.baseClient.patch(`/api/v2/sso-providers/${ssoProviderId}`, formData, options); }; @@ -682,7 +700,8 @@ class BHEAPIClient { permissionGet = (permissionId: string, options?: types.RequestOptions) => this.baseClient.get(`/api/v2/permissions/${permissionId}`, options); - getRoles = (options?: types.RequestOptions) => this.baseClient.get(`/api/v2/roles`, options); + getRoles = (options?: types.RequestOptions) => + this.baseClient.get(`/api/v2/roles`, options); getRole = (roleId: string, options?: types.RequestOptions) => this.baseClient.get(`/api/v2/roles/${roleId}`, options); diff --git a/packages/javascript/js-client-library/src/types.ts b/packages/javascript/js-client-library/src/types.ts index 470020a859..2574ab8510 100644 --- a/packages/javascript/js-client-library/src/types.ts +++ b/packages/javascript/js-client-library/src/types.ts @@ -149,14 +149,14 @@ export interface PutUserAuthSecretRequest { needsPasswordReset: boolean; } -export interface CreateSAMLProviderFormInputs { +export interface CreateSAMLProviderFormInputs extends SSOProviderConfiguration { name: string; metadata: FileList; } export type UpdateSAMLProviderFormInputs = Partial; export type UpsertSAMLProviderFormInputs = CreateSAMLProviderFormInputs | UpdateSAMLProviderFormInputs; -export interface CreateOIDCProviderRequest { +export interface CreateOIDCProviderRequest extends SSOProviderConfiguration { name: string; client_id: string; issuer: string; @@ -183,7 +183,17 @@ export interface OIDCProviderInfo extends Serial { sso_provider_id: number; } -export interface SSOProvider extends Serial { +export interface SSOProviderConfiguration { + config: { + auto_provision: { + enabled: boolean; + default_role_id: number; + role_provision: boolean; + }; + }; +} + +export interface SSOProvider extends Serial, SSOProviderConfiguration { name: string; slug: string; type: 'OIDC' | 'SAML'; @@ -214,12 +224,19 @@ interface Permission { authority: string; } -interface Role { +export interface Role { + id: number; name: string; description: string; permissions: Permission[]; } +export interface ListRolesResponse { + data: { + roles: Role[]; + }; +} + export interface ListUsersResponse { data: { users: User[]; From 2d7bf4b728b9e79a63114b660cf2d1d71e612aad Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 2 Jan 2025 16:13:22 -0800 Subject: [PATCH 9/9] BED-5110 added NoDataDialog component (#1046) * BED-5110 added NoDataDialog component * fixed test * added check for domains back in * passing query as props and fixing tests * addressed PR comments --- .../NoDataDialog/NoDataDialog.test.tsx | 50 +++++++++++++++++++ .../components/NoDataDialog/NoDataDialog.tsx | 44 ++++++++++++++++ .../src/components/NoDataDialog/index.tsx | 19 +++++++ .../bh-shared-ui/src/components/index.ts | 2 + 4 files changed, 115 insertions(+) create mode 100644 packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.test.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.tsx create mode 100644 packages/javascript/bh-shared-ui/src/components/NoDataDialog/index.tsx diff --git a/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.test.tsx b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.test.tsx new file mode 100644 index 0000000000..b9526d90e8 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.test.tsx @@ -0,0 +1,50 @@ +// Copyright 2023 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 + +import { render, screen } from '../../test-utils'; +import NoDataDialog from '.'; + +const gettingStartedLinkText = 'Getting Started guide'; +const fileIngestLinkText = 'start by uploading your data'; + +describe('NoDataDialog', () => { + it('should render', () => { + render( + {gettingStartedLinkText}} + fileIngestLink={<>{fileIngestLinkText}} + open={true} + /> + ); + + expect(screen.getByText('No Data Available')).toBeInTheDocument(); + expect(screen.getByText(/Getting Started guide/)).toBeInTheDocument(); + expect(screen.getByText(/start by uploading your data/)).toBeInTheDocument(); + }); + it('should not render when data is present', () => { + render( + {gettingStartedLinkText}} + fileIngestLink={<>{fileIngestLinkText}} + open={false} + /> + ); + + expect(screen.queryByText('No Data Available')).not.toBeInTheDocument(); + expect(screen.queryByText(/Getting Started guide/)).not.toBeInTheDocument(); + expect(screen.queryByText(/start by uploading your data/)).not.toBeInTheDocument(); + }); +}); diff --git a/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.tsx b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.tsx new file mode 100644 index 0000000000..796c4c1f1c --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/NoDataDialog.tsx @@ -0,0 +1,44 @@ +// Copyright 2023 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 + +import { Dialog, DialogContent, DialogDescription, DialogPortal, DialogTitle } from '@bloodhoundenterprise/doodleui'; + +type NoDataDialogProps = { + fileIngestLink: JSX.Element; + gettingStartedLink: JSX.Element; + open: boolean; +}; + +export const NoDataDialog: React.FC = ({ fileIngestLink, gettingStartedLink, open }) => { + return ( + { + // unblocks the body from being clickable so the user can go to another tab + document.body.style.pointerEvents = ''; + }}> + + + No Data Available + + To explore your environment, {fileIngestLink}, on the file ingest page. Need help? Check out the{' '} + {gettingStartedLink} for instructions. + + + + + ); +}; diff --git a/packages/javascript/bh-shared-ui/src/components/NoDataDialog/index.tsx b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/index.tsx new file mode 100644 index 0000000000..677198e822 --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/NoDataDialog/index.tsx @@ -0,0 +1,19 @@ +// Copyright 2023 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 + +import { NoDataDialog } from './NoDataDialog'; + +export default NoDataDialog; diff --git a/packages/javascript/bh-shared-ui/src/components/index.ts b/packages/javascript/bh-shared-ui/src/components/index.ts index 415009bc5e..c8f9b4d0df 100644 --- a/packages/javascript/bh-shared-ui/src/components/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/index.ts @@ -128,6 +128,8 @@ export { default as MenuItem } from './MenuItem'; export { default as NoDataAlert } from './NoDataAlert'; +export { default as NoDataDialog } from './NoDataDialog'; + export * from './NodeIcon'; export { default as NodeIcon } from './NodeIcon';