From 36971927a1fcf3c1c620375040112ae04a9c5e0e Mon Sep 17 00:00:00 2001 From: Shenali Date: Thu, 23 Jan 2025 01:03:54 +0530 Subject: [PATCH 01/22] Add external auth API integration and edit page content --- .../custom-authentication-create-wizard.tsx | 158 ++++++++++- .../components/edit/connection-edit.tsx | 19 +- .../custom-auth-general-details-form.tsx | 250 ++++++++++++++++++ .../edit/settings/general-settings.tsx | 49 +++- .../admin.connections.v1/models/connection.ts | 22 ++ .../pages/connection-edit.tsx | 9 +- 6 files changed, 481 insertions(+), 26 deletions(-) create mode 100644 features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx diff --git a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx index ea20a1cd55a..555f017d4c1 100644 --- a/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx +++ b/features/admin.connections.v1/components/create/custom-authentication-create-wizard.tsx @@ -20,8 +20,11 @@ import Backdrop from "@mui/material/Backdrop"; import Box from "@oxygen-ui/react/Box"; import Divider from "@oxygen-ui/react/Divider"; import InputAdornment from "@oxygen-ui/react/InputAdornment"; +import { EventPublisher } from "@wso2is/admin.core.v1"; import { ModalWithSidePanel } from "@wso2is/admin.core.v1/components"; -import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import { IdentityAppsError } from "@wso2is/core/errors"; +import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; +import { addAlert } from "@wso2is/core/store"; import { URLUtils } from "@wso2is/core/utils"; import { Field, Wizard2, WizardPage } from "@wso2is/form"; import { @@ -32,9 +35,14 @@ import { LinkButton, PrimaryButton, SelectionCard, - Steps + Steps, + useWizardAlert } from "@wso2is/react-components"; import { FormValidation } from "@wso2is/validation"; +import { AxiosError, AxiosResponse } from "axios"; +import cloneDeep from "lodash-es/cloneDeep"; +import isEmpty from "lodash-es/isEmpty"; +import kebabCase from "lodash-es/kebabCase"; import React, { FunctionComponent, MutableRefObject, @@ -47,27 +55,34 @@ import React, { useState } from "react"; import { useTranslation } from "react-i18next"; +import { useDispatch } from "react-redux"; +import { Dispatch } from "redux"; import { DropdownProps, Icon, Message, Grid as SemanticGrid } from "semantic-ui-react"; -import { useGetConnectionTemplate } from "../../api/connections"; +import { createConnection, useGetConnectionTemplate } from "../../api/connections"; import { getConnectionWizardStepIcons } from "../../configs/ui"; import { CommonAuthenticatorConstants } from "../../constants/common-authenticator-constants"; import { ConnectionUIConstants } from "../../constants/connection-ui-constants"; import { LocalAuthenticatorConstants } from "../../constants/local-authenticator-constants"; + import { AuthenticationType, AuthenticationTypeDropdownOption, AvailableCustomAuthentications, + ConnectionInterface, ConnectionTemplateInterface, CustomAuthenticationCreateWizardGeneralFormValuesInterface, EndpointConfigFormPropertyInterface, FormErrors, + GenericConnectionCreateWizardPropsInterface, WizardStepInterface, WizardStepsCustomAuth } from "../../models/connection"; import { ConnectionsManagementUtils } from "../../utils/connection-utils"; import "./custom-authentication-create-wizard.scss"; -export interface CustomAuthenticationCreateWizardPropsInterface extends IdentifiableComponentInterface { +export interface CustomAuthenticationCreateWizardPropsInterface + extends GenericConnectionCreateWizardPropsInterface, + IdentifiableComponentInterface { /** * Connection template interface. */ @@ -96,9 +111,11 @@ const CustomAuthenticationCreateWizard: FunctionComponent { const wizardRef: MutableRefObject = useRef(null); + const [ alert, setAlert, alertComponent ] = useWizardAlert(); const { CUSTOM_AUTHENTICATION_CONSTANTS: CustomAuthConstants } = ConnectionUIConstants; @@ -109,15 +126,17 @@ const CustomAuthenticationCreateWizard: FunctionComponent(null); - const [ isSubmitting ] = useState(false); + const [ isSubmitting, setIsSubmitting ] = useState(false); const [ showPrimarySecret, setShowPrimarySecret ] = useState(false); const [ showSecondarySecret, setShowSecondarySecret ] = useState(false); const [ authenticationType, setAuthenticationType ] = useState(null); const [ nextShouldBeDisabled, setNextShouldBeDisabled ] = useState(true); + const dispatch: Dispatch = useDispatch(); const { t } = useTranslation(); + const eventPublisher: EventPublisher = EventPublisher.getInstance(); - const { isLoading: isConnectionTemplateFetchRequestLoading } = useGetConnectionTemplate( + const { data: connectionTemplate, isLoading: isConnectionTemplateFetchRequestLoading } = useGetConnectionTemplate( selectedTemplateId, selectedTemplateId !== null ); @@ -133,11 +152,12 @@ const CustomAuthenticationCreateWizard: FunctionComponent { if (!initWizard) { - console.log("Init wizard"); setWizardSteps(getWizardSteps()); setInitWizard(true); } @@ -449,6 +469,127 @@ const CustomAuthenticationCreateWizard: FunctionComponent { + try { + return btoa(str); + } catch (error) { + return ""; + } + }; + + /** + * @param values - form values + * @param form - form instance + * @param callback - callback to proceed to the next step + */ + const handleFormSubmit = (values: any) => { + const FIRST_ENTRY: number = 0; + + const { idp: identityProvider } = cloneDeep(connectionTemplate); + const encodedIdentifier: string = encodeString(values?.identifier?.toString()); + + identityProvider.templateId = selectedTemplateId; + identityProvider.name = values?.displayName?.toString(); + + identityProvider.federatedAuthenticators.defaultAuthenticatorId = encodedIdentifier; + identityProvider.federatedAuthenticators.authenticators[FIRST_ENTRY].authenticatorId = encodedIdentifier; + identityProvider.federatedAuthenticators.authenticators[ + FIRST_ENTRY + ].endpoint.uri = values?.endpointUri.toString(); + identityProvider.federatedAuthenticators.authenticators[ + FIRST_ENTRY + ].endpoint.authentication.type = authenticationType; + + const authProperties: any = {}; + + authProperties["username"] = values?.usernameAuthProperty; + authProperties["password"] = values?.passwordAuthProperty; + identityProvider.federatedAuthenticators.authenticators[ + FIRST_ENTRY + ].endpoint.authentication.properties = authProperties; + + setIsSubmitting(true); + + createConnection(identityProvider) + .then((response: AxiosResponse) => { + eventPublisher.publish("connections-finish-adding-connection", { + type: _componentId + "-" + kebabCase(selectedAuthenticator) + }); + dispatch( + addAlert({ + description: t("authenticationProvider:notifications." + "addIDP.success.description"), + level: AlertLevels.SUCCESS, + message: t("authenticationProvider:notifications." + "addIDP.success.message") + }) + ); + // The created resource's id is sent as a location header. + // If that's available, navigate to the edit page. + if (!isEmpty(response.headers.location)) { + const location: string = response.headers.location; + const createdIdpID: string = location.substring(location.lastIndexOf("/") + 1); + + onIDPCreate(createdIdpID); + + return; + } + onIDPCreate(); + }) + .catch((error: AxiosError) => { + const identityAppsError: IdentityAppsError = ConnectionUIConstants.ERROR_CREATE_LIMIT_REACHED; + + if (error.response.status === 403 && error?.response?.data?.code === identityAppsError.getErrorCode()) { + setAlert({ + code: identityAppsError.getErrorCode(), + description: t(identityAppsError.getErrorDescription()), + level: AlertLevels.ERROR, + message: t(identityAppsError.getErrorMessage()), + traceId: identityAppsError.getErrorTraceId() + }); + setTimeout(() => setAlert(undefined), 4000); + + return; + } + + if (error?.response.status === 500 && error.response?.data.code === "IDP-65002") { + setAlert({ + description: t("authenticationProvider:notifications." + "addIDP.serverError.description"), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications." + "addIDP.serverError.message") + }); + setTimeout(() => setAlert(undefined), 8000); + + return; + } + + if (error.response && error.response.data && error.response.data.description) { + setAlert({ + description: t("authenticationProvider:notifications." + "addIDP.error.description", { + description: error.response.data.description + }), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications." + "addIDP.error.message") + }); + setTimeout(() => setAlert(undefined), 4000); + + return; + } + setAlert({ + description: t("authenticationProvider:notifications." + "addIDP.genericError.description"), + level: AlertLevels.ERROR, + message: t("authenticationProvider:notifications." + "addIDP.genericError.message") + }); + setTimeout(() => setAlert(undefined), 4000); + }) + .finally(() => { + setIsSubmitting(false); + }); + }; + const wizardCommonFirstPage = () => ( { @@ -811,6 +952,7 @@ const CustomAuthenticationCreateWizard: FunctionComponent setCurrentWizardStep(index) } data-componentid={ _componentId } diff --git a/features/admin.connections.v1/components/edit/connection-edit.tsx b/features/admin.connections.v1/components/edit/connection-edit.tsx index 121532382ea..a7292293f83 100644 --- a/features/admin.connections.v1/components/edit/connection-edit.tsx +++ b/features/admin.connections.v1/components/edit/connection-edit.tsx @@ -167,6 +167,7 @@ export const EditConnection: FunctionComponent = ( */ const [ isTrustedTokenIssuer, setIsTrustedTokenIssuer ] = useState(false); const [ isExpertMode, setIsExpertMode ] = useState(false); + const [ isCustomAuthenticator, setIsCustomAuthenticator ] = useState(false); const hasApplicationReadPermissions: boolean = useRequiredScopes(featureConfig?.applications?.scopes?.read); @@ -224,6 +225,7 @@ export const EditConnection: FunctionComponent = ( templateType={ type } isSaml={ isSaml } isOidc={ isOidc } + isCustomAuthenticator= { isCustomAuthenticator } editingIDP={ identityProvider } isLoading={ isLoading } onDelete={ onDelete } @@ -357,6 +359,11 @@ export const EditConnection: FunctionComponent = ( setIsTrustedTokenIssuer(type === CommonAuthenticatorConstants .CONNECTION_TEMPLATE_IDS.TRUSTED_TOKEN_ISSUER); setIsExpertMode(type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXPERT_MODE); + setIsCustomAuthenticator( + type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.EXTERNAL_CUSTOM_AUTHENTICATION || + type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.INTERNAL_CUSTOM_AUTHENTICATION || + type === CommonAuthenticatorConstants.CONNECTION_TEMPLATE_IDS.TWO_FACTOR_CUSTOM_AUTHENTICATION + ); }, [ type ]); useEffect(() => { @@ -446,6 +453,7 @@ export const EditConnection: FunctionComponent = ( // Evaluate whether to Show/Hide `Attributes`. if (shouldShowTab(type, ConnectionTabTypes.USER_ATTRIBUTES) && !isOrganizationEnterpriseAuthenticator + && !isCustomAuthenticator && (type !== CommonAuthenticatorConstants .CONNECTION_TEMPLATE_IDS.OIDC || isAttributesEnabledForOIDC) && (type !== CommonAuthenticatorConstants @@ -467,7 +475,8 @@ export const EditConnection: FunctionComponent = ( if (shouldShowTab(type, ConnectionTabTypes.IDENTITY_PROVIDER_GROUPS) && featureConfig?.identityProviderGroups?.enabled && - !isOrganizationEnterpriseAuthenticator) { + !isOrganizationEnterpriseAuthenticator + && !isCustomAuthenticator) { panes.push({ "data-tabid": ConnectionUIConstants.TabIds.IDENTITY_PROVIDER_GROUPS, menuItem: "Groups", @@ -477,7 +486,8 @@ export const EditConnection: FunctionComponent = ( if (shouldShowTab(type, ConnectionTabTypes.OUTBOUND_PROVISIONING) && identityProviderConfig.editIdentityProvider.showOutboundProvisioning && - !isOrganizationEnterpriseAuthenticator) { + !isOrganizationEnterpriseAuthenticator + && !isCustomAuthenticator) { panes.push({ "data-tabid": ConnectionUIConstants.TabIds.OUTBOUND_PROVISIONING, menuItem: "Outbound Provisioning", @@ -497,7 +507,8 @@ export const EditConnection: FunctionComponent = ( if (shouldShowTab(type, ConnectionTabTypes.ADVANCED) && identityProviderConfig.editIdentityProvider.showAdvancedSettings && - !isOrganizationEnterpriseAuthenticator) { + !isOrganizationEnterpriseAuthenticator + && !isCustomAuthenticator) { panes.push({ "data-tabid": ConnectionUIConstants.TabIds.ADVANCED, menuItem: "Advanced", @@ -528,7 +539,7 @@ export const EditConnection: FunctionComponent = ( if (!identityProvider || isLoading || ((!isOrganizationEnterpriseAuthenticator && !isTrustedTokenIssuer - && !isEnterpriseConnection && !isExpertMode) && !tabPaneExtensions)) { + && !isEnterpriseConnection && !isExpertMode && !isCustomAuthenticator) && !tabPaneExtensions)) { return ; } diff --git a/features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx b/features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx new file mode 100644 index 00000000000..fdf197fcea6 --- /dev/null +++ b/features/admin.connections.v1/components/edit/forms/custom-auth-general-details-form.tsx @@ -0,0 +1,250 @@ +/** + * Copyright (c) 2025, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); 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. + */ + +import { AppState, ConfigReducerStateInterface } from "@wso2is/admin.core.v1"; +import { IdentifiableComponentInterface } from "@wso2is/core/models"; +import { Field, Form } from "@wso2is/form"; +import { EmphasizedSegment } from "@wso2is/react-components"; +import { FormValidation } from "@wso2is/validation"; +import React, { FunctionComponent, ReactElement } from "react"; +import { useTranslation } from "react-i18next"; +import { useSelector } from "react-redux"; +import { ConnectionUIConstants } from "../../../constants/connection-ui-constants"; +import { + ConnectionInterface, + ConnectionListResponseInterface, + CustomAuthGeneralDetailsFormValuesInterface, + StrictConnectionInterface +} from "../../../models/connection"; + +/** + * Proptypes for the identity provider general details form component. + */ +interface CustomAuthGeneralDetailsFormPopsInterface extends IdentifiableComponentInterface { + /** + * Currently editing IDP. + */ + editingIDP?: ConnectionInterface; + /** + * Mark identity provider as primary. + */ + isPrimary?: boolean; + /** + * On submit callback. + */ + onSubmit: (values: any) => void; + /** + * Callback to update the idp details. + */ + onUpdate?: (id: string) => void; + /** + * Externally trigger form submission. + */ + triggerSubmit?: boolean; + /** + * Optimize for the creation wizard. + */ + enableWizardMode?: boolean; + /** + * List of available Idps. + */ + idpList?: ConnectionListResponseInterface; + /** + * Why? to hide or show the IdP logo edit input field. + * Introduced this for SAML and OIDC enterprise protocols. + * By default the icon/logo for this is readonly from + * extensions. + */ + hideIdPLogoEditField?: boolean; + /** + * Specifies if the component should only be read-only. + */ + isReadOnly: boolean; + /** + * Type of the template. + */ + templateType?: string; + /** + * Specifies if the form is submitting. + */ + isSubmitting?: boolean; +} + +const FORM_ID: string = "idp-custom-auth-general-details-form"; + +/** + * Form to edit general details of the custom authenticator. + * + * @param props - Props injected to the component. + * @returns Functional component. + */ +export const CustomAuthGeneralDetailsForm: FunctionComponent = ({ + onSubmit, + editingIDP, + idpList, + hideIdPLogoEditField, + isReadOnly, + templateType, + isSubmitting, + "data-componentid": _componentId = "idp-edit-custom-auth-general-settings-form" +}: CustomAuthGeneralDetailsFormPopsInterface): ReactElement => { + + const { t } = useTranslation(); + + /** + * Prepare form values for submitting. + * + * @param values - Form values. + * @returns Sanitized form values. + */ + const updateConfigurations = (values: CustomAuthGeneralDetailsFormValuesInterface): void => { + onSubmit({ + description: values.description?.toString(), + image: values.image?.toString(), + isPrimary: !!values.isPrimary, + name: values.name?.toString() + }); + }; + + /** + * Decode the encoded string. + * + * @param encodedStr - Encoded string. + * @returns Decoded string. + */ + const decodeString = (encodedStr: string): string => { + try { + return atob(encodedStr); + } catch (error) { + return ""; + } + }; + + return ( + + +
{ + updateConfigurations(values); + } } + data-componentid={ _componentId } + validate={ (values: CustomAuthGeneralDetailsFormValuesInterface) => { + const errors: Partial> = { + image: undefined + }; + + if (!FormValidation.isValidResourceName(values.name)) { + errors.name = t( + "customAuthentication:fields.createWizard.generalSettingsStep." + + "displayName.validations.invalid" + ); + } + + + return errors; + } } + > + + + + + { !isReadOnly && ( + + ) } + +
+
+ ); +}; diff --git a/features/admin.connections.v1/components/edit/settings/general-settings.tsx b/features/admin.connections.v1/components/edit/settings/general-settings.tsx index 53928c0cf15..c927834eb32 100755 --- a/features/admin.connections.v1/components/edit/settings/general-settings.tsx +++ b/features/admin.connections.v1/components/edit/settings/general-settings.tsx @@ -47,6 +47,7 @@ import { handleGetConnectionListCallError } from "../../../utils/connection-utils"; import { GeneralDetailsForm } from "../forms"; +import { CustomAuthGeneralDetailsForm } from "../forms/custom-auth-general-details-form"; /** * Proptypes for the identity provider general details component. @@ -86,6 +87,11 @@ interface GeneralSettingsInterface extends TestableComponentInterface { * IdP is a OIDC provider or not. */ isOidc?: boolean; + /** + * Explicitly specifies whether the currently displaying + * connector is a custom authenticator or not. + */ + isCustomAuthenticator?: boolean; /** * Type of the template. */ @@ -115,6 +121,7 @@ export const GeneralSettings: FunctionComponent = ( hideIdPLogoEditField, isSaml, isOidc, + isCustomAuthenticator, templateType, loader: Loader, [ "data-testid" ]: testId @@ -280,19 +287,35 @@ export const GeneralSettings: FunctionComponent = ( !isLoading && !isIdPListRequestLoading ? ( <> - + { + !isCustomAuthenticator ? ( + + ) : ( + () + ) + }