diff --git a/.changeset/tough-mugs-complain.md b/.changeset/tough-mugs-complain.md new file mode 100644 index 00000000000..cb7849e93c1 --- /dev/null +++ b/.changeset/tough-mugs-complain.md @@ -0,0 +1,7 @@ +--- +"@wso2is/admin.applications.v1": patch +"@wso2is/console": patch +"@wso2is/i18n": patch +--- + +Application warning banner. diff --git a/features/admin.applications.v1/models/application-inbound.ts b/features/admin.applications.v1/models/application-inbound.ts index ac4c4f8460d..e5a3a95f77c 100644 --- a/features/admin.applications.v1/models/application-inbound.ts +++ b/features/admin.applications.v1/models/application-inbound.ts @@ -193,6 +193,8 @@ export interface OIDCDataInterface { subject?: SubjectConfigInterface; isFAPIApplication?: boolean; hybridFlow?: HybridFlowConfigurationInterface; + useClientIdAsSubClaimForAppTokens?: boolean; + omitUsernameInIntrospectionRespForAppTokens?: boolean; } /** diff --git a/features/admin.applications.v1/pages/application-edit.scss b/features/admin.applications.v1/pages/application-edit.scss index 536ea9b575a..a48d76e3f90 100644 --- a/features/admin.applications.v1/pages/application-edit.scss +++ b/features/admin.applications.v1/pages/application-edit.scss @@ -19,3 +19,29 @@ .application-branding-link { cursor: pointer; } + +.ignore-once-button { + color: #788997; +} + +.banner-detail-card { + border: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; + background: #fffaf3; + padding: 5px; + padding-left: 35px; +} + +.application-outdated-alert-expanded-view { + border-bottom-left-radius: 0; + border-bottom-right-radius: 0; +} + +.spaced-list li { + margin-bottom: 10px; +} + +.banner-wrapper { + margin-bottom: 20px; +} diff --git a/features/admin.applications.v1/pages/application-edit.tsx b/features/admin.applications.v1/pages/application-edit.tsx index ba0cb6ccdcd..bfdf314ac29 100755 --- a/features/admin.applications.v1/pages/application-edit.tsx +++ b/features/admin.applications.v1/pages/application-edit.tsx @@ -16,6 +16,13 @@ * under the License. */ +import Alert from "@oxygen-ui/react/Alert"; +import AlertTitle from "@oxygen-ui/react/AlertTitle"; +import Button from "@oxygen-ui/react/Button"; +import Card from "@oxygen-ui/react/Card"; +import CardContent from "@oxygen-ui/react/CardContent"; +import Divider from "@oxygen-ui/react/Divider"; +import Grid from "@oxygen-ui/react/Grid"; import { useRequiredScopes } from "@wso2is/access-control"; import ApplicationTemplateMetadataProvider from "@wso2is/admin.application-templates.v1/provider/application-template-metadata-provider"; @@ -33,21 +40,29 @@ import { ExtensionTemplateListInterface } from "@wso2is/admin.template-core.v1/m import { isFeatureEnabled } from "@wso2is/core/helpers"; import { AlertLevels, IdentifiableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; +import { FormValue } from "@wso2is/form/src"; +import { Field, Forms } from "@wso2is/forms"; import { AnimatedAvatar, AppAvatar, + ConfirmationModal, + Hint, LabelWithPopup, Popup, TabPageLayout } from "@wso2is/react-components"; +import { AxiosError } from "axios"; +import classNames from "classnames"; import cloneDeep from "lodash-es/cloneDeep"; -import React, { FunctionComponent, ReactElement, useEffect, useMemo, useRef, useState } from "react"; -import { useTranslation } from "react-i18next"; +import React, { FunctionComponent, MutableRefObject, ReactElement, useEffect, useMemo, useRef, useState } from "react"; +import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { RouteComponentProps } from "react-router"; import { Dispatch } from "redux"; import { Label } from "semantic-ui-react"; +import { updateAuthProtocolConfig } from "../api/application"; import { useGetApplication } from "../api/use-get-application"; +import useGetApplicationInboundConfigs from "../api/use-get-application-inbound-configs"; import { EditApplication } from "../components/edit-application"; import { InboundProtocolDefaultFallbackTemplates } from "../components/meta/inbound-protocols.meta"; import { ApplicationManagementConstants } from "../constants"; @@ -57,6 +72,7 @@ import { ApplicationAccessTypes, ApplicationInterface, ApplicationTemplateListItemInterface, + OIDCDataInterface, State, SupportedAuthProtocolTypes, idpInfoTypeInterface @@ -129,6 +145,46 @@ const ApplicationEditPage: FunctionComponent = ( error: applicationGetRequestError } = useGetApplication(applicationId, !!applicationId); + const [ viewBannerDetails, setViewBannerDetails ] = useState(false); + const [ displayBanner, setDisplayBanner ] = useState(false); + const { + data: applicationInboundConfigData, + mutate: mutateApplicationInboundConfigs, + isLoading: isBannerDataLoading + } = useGetApplicationInboundConfigs(application?.id, SupportedAuthProtocolTypes.OIDC, !!application?.id); + const [ applicationInboundConfig, setApplicationInboundConfig ] = useState(undefined); + const [ useClientIdAsSubClaimForAppTokens, setUseClientIdAsSubClaimForAppTokens ] = useState(false); + const [ omitUsernameInIntrospectionRespForAppTokens, setOmitUsernameInIntrospectionRespForAppTokens ] + = useState(false); + const useClientIdAsSubClaimForAppTokensElement: MutableRefObject = useRef(); + const omitUsernameInIntrospectionRespForAppTokensElement: MutableRefObject = useRef(); + const [ bannerUpdateLoading, setBannerUpdateLoading ] = useState(false); + const [ showConfirmationModal, setShowConfirmationModal ] = useState(false); + const [ formData, setFormdata ] = useState(undefined); + const [ bannerSubmitDisabled, setBannerSubmitDisabled ] = useState(true); + + /** + * Loads banner data. + */ + useEffect(() => { + if (!isBannerDataLoading) { + setApplicationInboundConfig(applicationInboundConfigData); + } + }, [ applicationInboundConfigData, isBannerDataLoading ]); + + /** + * Assign loaded banner data into config states. + */ + useEffect(() => { + if (applicationInboundConfig != undefined) { + setUseClientIdAsSubClaimForAppTokens(applicationInboundConfig.useClientIdAsSubClaimForAppTokens); + setOmitUsernameInIntrospectionRespForAppTokens(applicationInboundConfig + .omitUsernameInIntrospectionRespForAppTokens); + setDisplayBanner(!applicationInboundConfig.useClientIdAsSubClaimForAppTokens + || !applicationInboundConfig.omitUsernameInIntrospectionRespForAppTokens); + } + }, [ applicationInboundConfig ]); + /** * Load the template that the application is built on. */ @@ -506,6 +562,355 @@ const ApplicationEditPage: FunctionComponent = ( return null; }; + /** + * Resolves the application banner content. + * + * @returns Alert banner. + */ + const resolveAlertBanner = (): ReactElement => { + const classes: any = classNames( { "application-outdated-alert-expanded-view": viewBannerDetails } ); + + return ( + !isBannerDataLoading && displayBanner && + ( +
+ + + + + ) + } + > + + } } > + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".alert.title") } + + + + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".alert.content") } + + + +
+ ) + ); + }; + + /** + * Resolves the application banner view details section. + * + * @returns Alert banner details. + */ + const resolveBannerViewDetails = (): ReactElement => { + return ( + + + { + !applicationInboundConfig + .useClientIdAsSubClaimForAppTokens && ( + <> + + ) => { + + if ( values.get("useClientIdAsSubClaimForAppTokens") + .length > 0) { + setBannerSubmitDisabled(false); + } else { + if (values.get("omitUsernameInIntrospectionRespForAppTokens") + .length === 0) { + setBannerSubmitDisabled(true); + } else { + setBannerSubmitDisabled(false); + } + } + setUseClientIdAsSubClaimForAppTokens( + !useClientIdAsSubClaimForAppTokens); + } + } + /> + + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens." + + "fields.useClientIdAsSubClaimForAppTokens.hint") } + + + ) + } + { + (!applicationInboundConfig.omitUsernameInIntrospectionRespForAppTokens + && !applicationInboundConfig.useClientIdAsSubClaimForAppTokens) && + ( + + + { showConfirmationModal && confirmationModal() } + + ); + }; + + /** + * Resolves the update confirmation modal. + * + * @returns Confirmation modal. + */ + const confirmationModal = () => { + return ( + setShowConfirmationModal(false) } + type="negative" + open={ showConfirmationModal } + assertionHint={ t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".confirmationModal.assertionHint") } + assertionType="checkbox" + primaryAction="Confirm" + secondaryAction="Cancel" + onSecondaryActionClick={ (): void =>{ + setShowConfirmationModal(false); + } } + onPrimaryActionClick={ handleBannerCheckBoxUpdateConfirmation } + closeOnDimmerClick={ false } + > + + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".confirmationModal.header") } + + + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".confirmationModal.message") } + + + { t("applications:forms.inboundOIDC.sections.legacyApplicationTokens" + + ".confirmationModal.content") } +
    + { formData["useClientIdAsSubClaimForAppTokens"] && ( +
  1. + { + t("applications:forms.inboundOIDC.sections" + + ".legacyApplicationTokens.fields." + + "useClientIdAsSubClaimForAppTokens.label") + } +
  2. + ) + } + { formData["omitUsernameInIntrospectionRespForAppTokens"] && ( +
  3. + { + t("applications:forms.inboundOIDC.sections" + + ".legacyApplicationTokens.fields." + + "omitUsernameInIntrospectionRespForAppTokens.label") + } +
  4. + ) + } +
+
+
+ ); + }; + + /** + * Handles banner content update action which prepares data. + */ + const handleBannerCheckBoxUpdate = (formDataValues: any) => { + mutateApplicationInboundConfigs().then((response: any) => { + + let values: any = response.data; + + if (formDataValues.get("omitUsernameInIntrospectionRespForAppTokens")){ + values = { + ...values, + omitUsernameInIntrospectionRespForAppTokens: + formDataValues.get("omitUsernameInIntrospectionRespForAppTokens")?.length > 0 + }; + } else { + values = { + ...values, + omitUsernameInIntrospectionRespForAppTokens: omitUsernameInIntrospectionRespForAppTokens + }; + } + + if (formDataValues.get("useClientIdAsSubClaimForAppTokens")) { + values = { + ...values, + useClientIdAsSubClaimForAppTokens: + formDataValues.get("useClientIdAsSubClaimForAppTokens")?.length > 0 + }; + } else { + values = { + ...values, + useClientIdAsSubClaimForAppTokens: useClientIdAsSubClaimForAppTokens + }; + } + + setFormdata({ ...values }); + setShowConfirmationModal(true); + }); + }; + + /** + * Handles the banner data update action. + */ + const handleBannerCheckBoxUpdateConfirmation = async (): Promise => { + setBannerUpdateLoading(true); + + return updateAuthProtocolConfig(application?.id, formData, SupportedAuthProtocolTypes.OIDC) + .then(() => { + dispatch(addAlert({ + description: t("applications:notifications.updateApplication" + + ".success.description"), + level: AlertLevels.SUCCESS, + message: t("applications:notifications.updateApplication" + + ".success.message") + })); + }) + .catch((error: AxiosError) => { + if (error?.response?.data?.description) { + dispatch(addAlert({ + description: error.response.data.description, + level: AlertLevels.ERROR, + message: t("applications:notifications.updateApplication" + + ".error.message") + })); + + return; + } + + dispatch(addAlert({ + description: t("applications:notifications.updateApplication" + + ".genericError.description"), + level: AlertLevels.ERROR, + message: t("applications:notifications.updateApplication" + + ".genericError.message") + })); + }).finally(() => { + setDisplayBanner(!formData.useClientIdAsSubClaimForAppTokens || + !formData.omitUsernameInIntrospectionRespForAppTokens); + setShowConfirmationModal(false); + setBannerUpdateLoading(false); + setViewBannerDetails(false); + mutateApplicationInboundConfigs(); + }); + }; + return ( = ( text: isConnectedAppsRedirect ? t("idp:connectedApps.applicationEdit.back", { idpName: callBackIdpName }) : t("console:develop.pages.applicationsEdit.backButton") } } + alertBanner={ resolveAlertBanner() } titleTextAlign="left" bottomMargin={ false } pageHeaderMaxWidth={ true } diff --git a/modules/i18n/src/models/namespaces/applications-ns.ts b/modules/i18n/src/models/namespaces/applications-ns.ts index 575a05ba8f5..9625aa67087 100644 --- a/modules/i18n/src/models/namespaces/applications-ns.ts +++ b/modules/i18n/src/models/namespaces/applications-ns.ts @@ -1377,6 +1377,34 @@ export interface ApplicationsNS { }; }; }; + legacyApplicationTokens: { + heading: string; + alert : { + title: string; + content: string; + viewButton: string; + cancelButton: string; + } + confirmationModal: { + assertionHint: string; + header: string; + message: string; + content: string; + }, + fields: { + commonInstruction: string; + useClientIdAsSubClaimForAppTokens: { + instruction: string; + label: string; + hint: string; + }; + omitUsernameInIntrospectionRespForAppTokens: { + instruction: string; + label: string; + hint: string; + }; + } + }; logoutURLs: { heading: string; fields: { diff --git a/modules/i18n/src/translations/en-US/portals/applications.ts b/modules/i18n/src/translations/en-US/portals/applications.ts index 3795c1d7c6b..7585bb32726 100644 --- a/modules/i18n/src/translations/en-US/portals/applications.ts +++ b/modules/i18n/src/translations/en-US/portals/applications.ts @@ -1643,6 +1643,39 @@ export const applications: ApplicationsNS = { }, heading: "ID Token" }, + legacyApplicationTokens: { + heading: "Legacy Application Tokens", + alert : { + title: "Application is outdated.", + content: "This application is using an outdated behavior for application tokens.", + viewButton: "View Details", + cancelButton: "Ignore Once" + }, + confirmationModal: { + header: "Have you done the relevant changes?", + message: "Proceeding the action without making relevant change will cause the client application behavior break.", + content: "Confirming the action will,", + assertionHint: "Please confirm your action" + }, + fields: { + commonInstruction: "Change the customer-end applications accordingly to recieve the below updates.", + useClientIdAsSubClaimForAppTokens: { + instruction: "Application access token sub attribute will be client_id generated for an application.", + label: "Set client_id as the sub attribute value for Application tokens", + hint: "For application tokens, the sub attribute was previosuly set to the " + + "application owner's user_id. However, to support a more industry standard " + + "solution, this value will be changed to the client ID for application tokens." + }, + omitUsernameInIntrospectionRespForAppTokens: { + instruction: "Introspection response for application access token will not include the username attribute.", + label: "Omit sending username attribute in the Introspection response for Application tokens", + hint: "For access tokens, the previous behavior includes sending the username attribute" + + " in the introspection response. However, to support a more industry standard" + + " solution, the introspection response for application tokens will no longer" + + " include the username attribute." + } + } + }, logoutURLs: { fields: { back: { diff --git a/modules/react-components/src/components/page-header/page-header.tsx b/modules/react-components/src/components/page-header/page-header.tsx index 7b446802d8e..a36cf5e8134 100644 --- a/modules/react-components/src/components/page-header/page-header.tsx +++ b/modules/react-components/src/components/page-header/page-header.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). All Rights Reserved. + * Copyright (c) 2020, 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 @@ -41,6 +41,10 @@ export interface PageHeaderPropsInterface extends LoadableComponentInterface, Te * Column width for the action container grid. */ actionColumnWidth?: SemanticWIDTHS; + /** + * Alert component displayed in the page header. + */ + alertBanner?: ReactNode; /** * Go back button. */ @@ -127,6 +131,7 @@ export const PageHeader: React.FunctionComponent = ( const { action, actionColumnWidth, + alertBanner, backButton, bottomMargin, className, @@ -301,6 +306,9 @@ export const PageHeader: React.FunctionComponent = ( ) ) } + { + alertBanner + } { action ? (