diff --git a/packages/go/openapi/doc/openapi.json b/packages/go/openapi/doc/openapi.json index 75c1fe361b..31b8cb071f 100644 --- a/packages/go/openapi/doc/openapi.json +++ b/packages/go/openapi/doc/openapi.json @@ -605,7 +605,7 @@ "type": "object", "properties": { "data": { - "$ref": "#/components/schemas/model.auth-provider" + "$ref": "#/components/schemas/model.oidc-provider" } } } @@ -646,6 +646,100 @@ } } ], + "patch": { + "operationId": "PatchSSOProvider", + "summary": "Update SSO Provider", + "description": "Updates an existing SSO provider. Updating saml provider requires a \"multipart/form-data\" body. Updating oidc provider requires \"application/json\" body. Response is respective provider", + "tags": [ + "Auth", + "Community", + "Enterprise" + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "properties": { + "name": { + "type": "string", + "description": "Name of the new SAML provider." + }, + "metadata": { + "type": "string", + "format": "binary", + "description": "Metadata XML file." + } + } + } + }, + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the OIDC provider" + }, + "issuer": { + "type": "string", + "format": "url", + "description": "URL of the OIDC issuer" + }, + "client_id": { + "type": "string", + "description": "Client ID for the OIDC provider" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/model.saml-provider" + } + } + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/components/schemas/model.oidc-provider" + } + } + } + ] + } + } + } + }, + "401": { + "$ref": "#/components/responses/unauthorized" + }, + "403": { + "$ref": "#/components/responses/forbidden" + }, + "404": { + "$ref": "#/components/responses/not-found" + }, + "429": { + "$ref": "#/components/responses/too-many-requests" + }, + "500": { + "$ref": "#/components/responses/internal-server-error" + } + } + }, "delete": { "operationId": "DeleteSSOProvider", "summary": "Delete SSO Provider", 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 e174da4166..0a82e816fc 100644 --- a/packages/go/openapi/src/paths/sso.sso-providers.id.yaml +++ b/packages/go/openapi/src/paths/sso.sso-providers.id.yaml @@ -23,6 +23,66 @@ parameters: schema: type: integer format: int32 +patch: + operationId: PatchSSOProvider + summary: Update SSO Provider + description: Updates an existing SSO provider. Updating saml provider requires a "multipart/form-data" body. Updating oidc provider requires "application/json" body. Response is respective provider + tags: + - Auth + - Community + - Enterprise + requestBody: + required: true + content: + multipart/form-data: + schema: + properties: + name: + type: string + description: Name of the new SAML provider. + metadata: + type: string + format: binary + description: Metadata XML file. + application/json: + schema: + type: object + properties: + name: + type: string + description: Name of the OIDC provider + issuer: + type: string + format: url + description: URL of the OIDC issuer + client_id: + type: string + description: Client ID for the OIDC provider + responses: + '200': + description: OK + content: + application/json: + schema: + oneOf: + - type: object + properties: + data: + $ref: './../schemas/model.saml-provider.yaml' + - type: object + properties: + data: + $ref: './../schemas/model.oidc-provider.yaml' + '401': + $ref: './../responses/unauthorized.yaml' + '403': + $ref: './../responses/forbidden.yaml' + '404': + $ref: './../responses/not-found.yaml' + '429': + $ref: './../responses/too-many-requests.yaml' + '500': + $ref: './../responses/internal-server-error.yaml' delete: operationId: DeleteSSOProvider summary: Delete SSO Provider 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 d9a71bea4f..777ad47cdc 100644 --- a/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml +++ b/packages/go/openapi/src/paths/sso.sso-providers.oidc.yaml @@ -54,7 +54,7 @@ post: type: object properties: data: - $ref: './../schemas/model.auth-provider.yaml' + $ref: './../schemas/model.oidc-provider.yaml' '400': $ref: './../responses/bad-request.yaml' '401': diff --git a/packages/javascript/bh-shared-ui/src/components/CreateOIDCProviderDialog.tsx b/packages/javascript/bh-shared-ui/src/components/CreateOIDCProviderDialog.tsx deleted file mode 100644 index f5e5f91600..0000000000 --- a/packages/javascript/bh-shared-ui/src/components/CreateOIDCProviderDialog.tsx +++ /dev/null @@ -1,159 +0,0 @@ -// 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, Dialog, DialogTitle, DialogContent, DialogActions, Grid, TextField } from '@mui/material'; -import { FC } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import { CreateOIDCProviderRequest } from 'js-client-library'; - -const CreateOIDCProviderDialog: FC<{ - open: boolean; - error?: string; - onClose: () => void; - onSubmit: (data: CreateOIDCProviderRequest) => void; -}> = ({ open, error, onClose, onSubmit: _onSubmit }) => { - const { - control, - handleSubmit, - reset, - formState: { errors }, - } = useForm({ - defaultValues: { - name: '', - client_id: '', - issuer: '', - }, - }); - - const onSubmit = (data: CreateOIDCProviderRequest) => { - _onSubmit(data); - reset(); - }; - - const handleClose = () => { - onClose(); - reset(); - }; - - return ( - - Create OIDC Provider -
- - - - ( - - )} - /> - - - ( - - )} - /> - - - ( - - )} - /> - - {error && ( - - {error} - - )} - - - - - - -
-
- ); -}; - -export default CreateOIDCProviderDialog; diff --git a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.tsx b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.tsx index b2bfb55de9..852e106ceb 100644 --- a/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.tsx +++ b/packages/javascript/bh-shared-ui/src/components/SSOProviderTable/SSOProviderTable.tsx @@ -15,7 +15,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Button } from '@bloodhoundenterprise/doodleui'; -import { faEllipsisVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; +import { faEdit, faEllipsisVertical, faTrash } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { IconButton, @@ -61,7 +61,8 @@ const StyledMenu = withStyles({ const SSOProviderTableActionsMenu: FC<{ onDeleteSSOProvider: () => void; -}> = ({ onDeleteSSOProvider }) => { + onUpdateSSOProvider: () => void; +}> = ({ onDeleteSSOProvider, onUpdateSSOProvider }) => { /* Hooks */ const [anchorEl, setAnchorEl] = useState(null); @@ -79,6 +80,11 @@ const SSOProviderTableActionsMenu: FC<{ setAnchorEl(null); }; + const onClickUpdateSSOProvider = () => { + onUpdateSSOProvider(); + setAnchorEl(null); + }; + return ( <> @@ -97,6 +103,12 @@ const SSOProviderTableActionsMenu: FC<{ + + + + + + ); @@ -105,11 +117,20 @@ const SSOProviderTableActionsMenu: FC<{ const SSOProviderTable: FC<{ ssoProviders: SSOProvider[]; loading: boolean; - onDeleteSSOProvider: (ssoProviderId: SSOProvider['id']) => void; + onDeleteSSOProvider: (ssoProvider: SSOProvider) => void; + onUpdateSSOProvider: (ssoProvider: SSOProvider) => void; onClickSSOProvider: (ssoProviderId: SSOProvider['id']) => void; onToggleTypeSortOrder: () => void; typeSortOrder?: SortOrder; -}> = ({ ssoProviders, loading, onDeleteSSOProvider, onClickSSOProvider, typeSortOrder, onToggleTypeSortOrder }) => { +}> = ({ + ssoProviders, + loading, + onDeleteSSOProvider, + onUpdateSSOProvider, + onClickSSOProvider, + onToggleTypeSortOrder, + typeSortOrder, +}) => { const theme = useTheme(); return ( @@ -178,7 +199,8 @@ const SSOProviderTable: FC<{ onDeleteSSOProvider(ssoProvider.id)} + onDeleteSSOProvider={() => onDeleteSSOProvider(ssoProvider)} + onUpdateSSOProvider={() => onUpdateSSOProvider(ssoProvider)} /> onClickSSOProvider(ssoProvider.id)} size='small'> diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx new file mode 100644 index 0000000000..3cda8ded0e --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderDialog.tsx @@ -0,0 +1,49 @@ +// 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 { Dialog, DialogTitle } from '@mui/material'; +import { SSOProvider, UpsertOIDCProviderRequest } from 'js-client-library'; +import UpsertOIDCProviderForm from './UpsertOIDCProviderForm'; + +const UpsertOIDCProviderDialog: React.FC<{ + open: boolean; + error?: string; + oldSSOProvider?: SSOProvider; + onClose: () => void; + onSubmit: (data: UpsertOIDCProviderRequest) => void; +}> = ({ open, error, oldSSOProvider, onClose, onSubmit }) => { + return ( + + {oldSSOProvider ? 'Edit' : 'Create'} OIDC Provider + + + ); +}; + +export default UpsertOIDCProviderDialog; diff --git a/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx new file mode 100644 index 0000000000..b99c8ec04b --- /dev/null +++ b/packages/javascript/bh-shared-ui/src/components/UpsertOIDCProviderForm.tsx @@ -0,0 +1,139 @@ +// 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 { FC } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { OIDCProviderInfo, SSOProvider, UpsertOIDCProviderRequest } from 'js-client-library'; + +const UpsertOIDCProviderForm: FC<{ + error?: string; + oldSSOProvider?: SSOProvider; + onClose: () => void; + onSubmit: (data: UpsertOIDCProviderRequest) => void; +}> = ({ error, oldSSOProvider, onClose, onSubmit }) => { + const defaultValues = { + name: oldSSOProvider?.name ?? '', + client_id: (oldSSOProvider?.details as OIDCProviderInfo)?.client_id ?? '', + issuer: (oldSSOProvider?.details as OIDCProviderInfo)?.issuer ?? '', + }; + + const { + control, + handleSubmit, + reset, + formState: { errors }, + } = useForm({ defaultValues }); + + const handleClose = () => { + onClose(); + reset(); + }; + + return ( +
+ + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + {error && ( + + {error} + + )} + + + + + + +
+ ); +}; + +export default UpsertOIDCProviderForm; diff --git a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/CreateSAMLProviderDialog.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx similarity index 60% rename from packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/CreateSAMLProviderDialog.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx index 0cef28fe55..cfc85ed2e1 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/CreateSAMLProviderDialog.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/UpsertSAMLProviderDialog.tsx @@ -15,15 +15,16 @@ // SPDX-License-Identifier: Apache-2.0 import { Dialog, DialogTitle } from '@mui/material'; -import CreateSAMLProviderForm from '../CreateSAMLProviderForm'; -import { CreateSAMLProviderFormInputs } from '../CreateSAMLProviderForm/CreateSAMLProviderForm'; +import { SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; +import UpsertSAMLProviderForm from '../UpsertSAMLProviderForm'; -const CreateSAMLProviderDialog: React.FC<{ +const UpsertSAMLProviderDialog: React.FC<{ open: boolean; error?: string; + oldSSOProvider?: SSOProvider; onClose: () => void; - onSubmit: (data: CreateSAMLProviderFormInputs) => void; -}> = ({ open, error, onClose, onSubmit }) => { + onSubmit: (data: UpsertSAMLProviderFormInputs) => void; +}> = ({ open, error, oldSSOProvider, onClose, onSubmit }) => { return ( - Create SAML Provider - + {oldSSOProvider ? 'Edit' : 'Create'} SAML Provider + ); }; -export default CreateSAMLProviderDialog; +export default UpsertSAMLProviderDialog; diff --git a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/index.ts b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/index.ts similarity index 86% rename from packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/index.ts rename to packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/index.ts index 42cdc842b2..042c9c18a2 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderDialog/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderDialog/index.ts @@ -14,5 +14,5 @@ // // SPDX-License-Identifier: Apache-2.0 -export * from './CreateSAMLProviderDialog'; -export { default } from './CreateSAMLProviderDialog'; +export * from './UpsertSAMLProviderDialog'; +export { default } from './UpsertSAMLProviderDialog'; diff --git a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/CreateSAMLProviderForm.test.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx similarity index 88% rename from packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/CreateSAMLProviderForm.test.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx index 9cda4c633d..518af2ec47 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/CreateSAMLProviderForm.test.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.test.tsx @@ -16,13 +16,13 @@ import userEvent from '@testing-library/user-event'; import { render, screen, waitFor } from '../../test-utils'; -import CreateSAMLProviderForm from './CreateSAMLProviderForm'; +import UpsertSAMLProviderForm from './UpsertSAMLProviderForm'; -describe('CreateSAMLProviderForm', () => { +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('CreateSAMLProviderForm', () => { 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('CreateSAMLProviderForm', () => { 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('CreateSAMLProviderForm', () => { 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/CreateSAMLProviderForm/CreateSAMLProviderForm.tsx b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx similarity index 90% rename from packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/CreateSAMLProviderForm.tsx rename to packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx index 4eba8a6bc2..af8d98fef6 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/CreateSAMLProviderForm.tsx +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/UpsertSAMLProviderForm.tsx @@ -28,27 +28,23 @@ import { } from '@mui/material'; import { useState, FC } from 'react'; import { Controller, useForm } from 'react-hook-form'; +import { SSOProvider, UpsertSAMLProviderFormInputs } from 'js-client-library'; -export interface CreateSAMLProviderFormInputs { - name: string; - metadata: FileList; -} - -const CreateSAMLProviderForm: FC<{ +const UpsertSAMLProviderForm: FC<{ error?: string; + oldSSOProvider?: SSOProvider; onClose: () => void; - onSubmit: (data: CreateSAMLProviderFormInputs) => void; -}> = ({ error, onClose, onSubmit }) => { + onSubmit: (data: UpsertSAMLProviderFormInputs) => void; +}> = ({ error, onClose, oldSSOProvider, onSubmit }) => { const theme = useTheme(); const { control, handleSubmit, reset, - formState: { errors }, - } = useForm({ + } = useForm({ defaultValues: { - name: '', + name: oldSSOProvider?.name ?? '', metadata: undefined, }, }); @@ -61,7 +57,7 @@ const CreateSAMLProviderForm: FC<{ }; return ( -
+ @@ -96,9 +92,7 @@ const CreateSAMLProviderForm: FC<{ ( @@ -148,11 +142,11 @@ const CreateSAMLProviderForm: FC<{ Cancel ); }; -export default CreateSAMLProviderForm; +export default UpsertSAMLProviderForm; diff --git a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/index.ts b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/index.ts similarity index 86% rename from packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/index.ts rename to packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/index.ts index d11eb29d7c..0da797b75a 100644 --- a/packages/javascript/bh-shared-ui/src/components/CreateSAMLProviderForm/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/UpsertSAMLProviderForm/index.ts @@ -14,5 +14,5 @@ // // SPDX-License-Identifier: Apache-2.0 -export * from './CreateSAMLProviderForm'; -export { default } from './CreateSAMLProviderForm'; +export * from './UpsertSAMLProviderForm'; +export { default } from './UpsertSAMLProviderForm'; diff --git a/packages/javascript/bh-shared-ui/src/components/index.ts b/packages/javascript/bh-shared-ui/src/components/index.ts index 38bb788996..5d6919bc43 100644 --- a/packages/javascript/bh-shared-ui/src/components/index.ts +++ b/packages/javascript/bh-shared-ui/src/components/index.ts @@ -49,12 +49,6 @@ export { default as ConfirmationDialog } from './ConfirmationDialog'; export * from './CreateMenu'; export { default as CreateMenu } from './CreateMenu'; -export * from './CreateSAMLProviderDialog'; -export { default as CreateSAMLProviderDialog } from './CreateSAMLProviderDialog'; - -export * from './CreateSAMLProviderForm'; -export { default as CreateSAMLProviderForm } from './CreateSAMLProviderForm'; - export * from './CreateUserForm'; export { default as CreateUserForm } from './CreateUserForm'; @@ -176,6 +170,12 @@ 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 './UpsertSAMLProviderForm'; +export { default as UpsertSAMLProviderForm } from './UpsertSAMLProviderForm'; + export * from './UserTokenManagementDialog'; export { default as UserTokenManagementDialog } from './UserTokenManagementDialog'; 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 96868063c7..cfcdd40915 100644 --- a/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx +++ b/packages/javascript/bh-shared-ui/src/views/SSOConfiguration/SSOConfiguration.tsx @@ -17,20 +17,19 @@ import { faSearch } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Box, Grid, Paper, TextField, Typography, useTheme } from '@mui/material'; -import { CreateOIDCProviderRequest, SSOProvider } from 'js-client-library'; +import { SSOProvider, UpsertOIDCProviderRequest, UpsertSAMLProviderFormInputs } from 'js-client-library'; import { ChangeEvent, FC, useMemo, useState } from 'react'; import { useMutation, useQuery } from 'react-query'; import { ConfirmationDialog, CreateMenu, - CreateSAMLProviderDialog, - CreateSAMLProviderFormInputs, DocumentationLinks, PageWithTitle, SSOProviderInfoPanel, SSOProviderTable, + UpsertSAMLProviderDialog, } from '../../components'; -import CreateOIDCProviderDialog from '../../components/CreateOIDCProviderDialog'; +import UpsertOIDCProviderDialog from '../../components/UpsertOIDCProviderDialog'; import { useFeatureFlag } from '../../hooks'; import { useNotifications } from '../../providers'; import { SortOrder, apiClient } from '../../utils'; @@ -42,10 +41,10 @@ const SSOConfiguration: FC = () => { const { data: flag } = useFeatureFlag('oidc_support'); const [selectedSSOProviderId, setSelectedSSOProviderId] = useState(); - const [ssoProviderIdToDelete, setSSOProviderIdToDelete] = useState(); + const [ssoProviderIdToDeleteOrUpdate, setSSOProviderIdToDeleteOrUpdate] = useState(); const [dialogOpen, setDialogOpen] = useState<'SAML' | 'OIDC' | 'DELETE' | ''>(''); const [nameFilter, setNameFilter] = useState(''); - const [createProviderError, setCreateProviderError] = useState(''); + const [upsertProviderError, setUpsertProviderError] = useState(''); const [typeSortOrder, setTypeSortOrder] = useState(); const listSSOProvidersQuery = useQuery(['listSSOProviders'], ({ signal }) => @@ -97,6 +96,10 @@ const SSOConfiguration: FC = () => { return listSSOProvidersQuery.data?.find(({ id }) => id === selectedSSOProviderId); }, [selectedSSOProviderId, listSSOProvidersQuery.data]); + const selectedSSOProviderToUpdate = useMemo(() => { + return listSSOProvidersQuery.data?.find(({ id }) => id === ssoProviderIdToDeleteOrUpdate); + }, [ssoProviderIdToDeleteOrUpdate, listSSOProvidersQuery.data]); + /* Event Handlers */ const openSAMLProviderDialog = () => { @@ -112,24 +115,39 @@ const SSOConfiguration: FC = () => { }; const closeDialog = () => { + setUpsertProviderError(''); setDialogOpen(''); - setCreateProviderError(''); + setTimeout(() => setSSOProviderIdToDeleteOrUpdate(undefined), 500); }; const onClickSSOProvider = (ssoProviderId: SSOProvider['id']) => { setSelectedSSOProviderId(ssoProviderId); }; - const onSelectDeleteSSOProvider = (ssoProviderId: SSOProvider['id']) => { - setSSOProviderIdToDelete(ssoProviderId); - openDeleteProviderDialog(); + const onSelectDeleteOrUpdateSSOProvider = (action: 'DELETE' | 'UPDATE') => (ssoProvider: SSOProvider) => { + setSSOProviderIdToDeleteOrUpdate(ssoProvider.id); + switch (action) { + case 'DELETE': + openDeleteProviderDialog(); + break; + case 'UPDATE': + switch (ssoProvider.type) { + case 'SAML': + openSAMLProviderDialog(); + break; + case 'OIDC': + openOIDCProviderDialog(); + break; + } + break; + } }; const onDeleteSSOProvider = async (response: boolean) => { let errored = false; - if (response && ssoProviderIdToDelete) { + if (response && ssoProviderIdToDeleteOrUpdate) { try { - await deleteSSOProviderMutation.mutateAsync(ssoProviderIdToDelete); + await deleteSSOProviderMutation.mutateAsync(ssoProviderIdToDeleteOrUpdate); } catch (err: any) { if (err?.response?.status !== 404) { errored = true; @@ -152,27 +170,48 @@ const SSOConfiguration: FC = () => { } }; - const createSAMLProvider = async (samlProvider: CreateSAMLProviderFormInputs) => { - setCreateProviderError(''); + const upsertSAMLProvider = async (samlProvider: UpsertSAMLProviderFormInputs) => { + setUpsertProviderError(''); try { - await apiClient.createSAMLProviderFromFile({ ...samlProvider, metadata: samlProvider.metadata[0] }); + const payload = { name: samlProvider.name, metadata: samlProvider.metadata && samlProvider.metadata[0] }; + if (ssoProviderIdToDeleteOrUpdate) { + await apiClient.updateSAMLProviderFromFile(ssoProviderIdToDeleteOrUpdate, payload); + } else { + if (payload.name && payload.metadata) { + await apiClient.createSAMLProviderFromFile({ name: payload.name, metadata: payload.metadata }); + } + } listSSOProvidersQuery.refetch(); closeDialog(); } catch (error) { console.error(error); - setCreateProviderError('Unable to create new SAML Provider configuration. Please try again.'); + setUpsertProviderError( + `Unable to ${ssoProviderIdToDeleteOrUpdate ? 'update' : 'create new'} SAML Provider configuration. Please try again.` + ); } }; - const createOIDCProvider = async (oidcProvider: CreateOIDCProviderRequest) => { - setCreateProviderError(''); + const upsertOIDCProvider = async (oidcProvider: UpsertOIDCProviderRequest) => { + setUpsertProviderError(''); try { - await apiClient.createOIDCProvider(oidcProvider); + if (ssoProviderIdToDeleteOrUpdate) { + await apiClient.updateOIDCProvider(ssoProviderIdToDeleteOrUpdate, oidcProvider); + } else { + if (oidcProvider.name && oidcProvider.client_id && oidcProvider.issuer) { + await apiClient.createOIDCProvider({ + name: oidcProvider.name, + client_id: oidcProvider.client_id, + issuer: oidcProvider.issuer, + }); + } + } listSSOProvidersQuery.refetch(); closeDialog(); } catch (error) { console.error(error); - setCreateProviderError('Unable to create new OIDC Provider configuration. Please try again.'); + setUpsertProviderError( + `Unable to ${ssoProviderIdToDeleteOrUpdate ? 'update' : 'create new'} OIDC Provider configuration. Please try again.` + ); } }; @@ -235,7 +274,8 @@ const SSOConfiguration: FC = () => { ssoProviders={ssoProviders} loading={listSSOProvidersQuery.isLoading} onClickSSOProvider={onClickSSOProvider} - onDeleteSSOProvider={onSelectDeleteSSOProvider} + onDeleteSSOProvider={onSelectDeleteOrUpdateSSOProvider('DELETE')} + onUpdateSSOProvider={onSelectDeleteOrUpdateSSOProvider('UPDATE')} typeSortOrder={typeSortOrder} onToggleTypeSortOrder={toggleTypeSortOrder} /> @@ -248,17 +288,19 @@ const SSOConfiguration: FC = () => { )} - - this.baseClient.get(`/api/v2/saml/providers/${samlProviderId}`, options); - createSAMLProvider = ( - data: { - name: string; - displayName: string; - signingCertificate: string; - issuerUri: string; - singleSignOnUri: string; - principalAttributeMappings: string[]; - }, - options?: types.RequestOptions - ) => - this.baseClient.post( - `/api/v2/saml`, - { - name: data.name, - display_name: data.displayName, - signing_certificate: data.signingCertificate, - issuer_uri: data.issuerUri, - single_signon_uri: data.singleSignOnUri, - principal_attribute_mappings: data.principalAttributeMappings, - }, - options - ); - createSAMLProviderFromFile = (data: { name: string; metadata: File }, options?: types.RequestOptions) => { const formData = new FormData(); formData.append('name', data.name); @@ -673,6 +649,21 @@ class BHEAPIClient { return this.baseClient.post(`/api/v2/saml/providers`, formData, options); }; + updateSAMLProviderFromFile = ( + ssoProviderId: types.SSOProvider['id'], + data: { name?: string; metadata?: File }, + options?: types.RequestOptions + ) => { + const formData = new FormData(); + if (data.name) { + formData.append('name', data.name); + } + if (data.metadata) { + formData.append('metadata', data.metadata); + } + return this.baseClient.patch(`/api/v2/sso-providers/${ssoProviderId}`, formData, options); + }; + validateSAMLProvider = ( data: { name: string; @@ -703,6 +694,9 @@ class BHEAPIClient { createOIDCProvider = (oidcProvider: types.CreateOIDCProviderRequest) => this.baseClient.post(`/api/v2/sso-providers/oidc`, oidcProvider); + updateOIDCProvider = (ssoProviderId: types.SSOProvider['id'], oidcProvider: types.UpdateOIDCProviderRequest) => + this.baseClient.patch(`/api/v2/sso-providers/${ssoProviderId}`, oidcProvider); + listSSOProviders = (options?: types.RequestOptions) => this.baseClient.get(`/api/v2/sso-providers`, options); diff --git a/packages/javascript/js-client-library/src/types.ts b/packages/javascript/js-client-library/src/types.ts index 9b851c7e43..470020a859 100644 --- a/packages/javascript/js-client-library/src/types.ts +++ b/packages/javascript/js-client-library/src/types.ts @@ -149,11 +149,20 @@ export interface PutUserAuthSecretRequest { needsPasswordReset: boolean; } +export interface CreateSAMLProviderFormInputs { + name: string; + metadata: FileList; +} +export type UpdateSAMLProviderFormInputs = Partial; +export type UpsertSAMLProviderFormInputs = CreateSAMLProviderFormInputs | UpdateSAMLProviderFormInputs; + export interface CreateOIDCProviderRequest { name: string; client_id: string; issuer: string; } +export type UpdateOIDCProviderRequest = Partial; +export type UpsertOIDCProviderRequest = CreateOIDCProviderRequest | UpdateOIDCProviderRequest; export interface SAMLProviderInfo extends Serial { name: string;