From ff8147c1c6acffb5b7e19a1bb347c283e3d21b64 Mon Sep 17 00:00:00 2001 From: Qi Liu Date: Fri, 22 Nov 2024 14:41:55 -0800 Subject: [PATCH] disable optional interactive user for http 20 or min tls version 1.3 --- client-react/src/models/site/site.ts | 3 +- .../app-settings/AppSettingsDataLoader.tsx | 14 +-- .../app/app-settings/AppSettingsFormData.ts | 39 +++++- .../GeneralSettings/ClientCert/ClientCert.tsx | 111 +++++++----------- .../app-settings/GeneralSettings/Platform.tsx | 8 +- .../src/app/shared/models/portal-resources.ts | 8 +- server/Resources/Resources.resx | 15 ++- 7 files changed, 100 insertions(+), 98 deletions(-) diff --git a/client-react/src/models/site/site.ts b/client-react/src/models/site/site.ts index 1eb08f98eb..166e3d3cf0 100644 --- a/client-react/src/models/site/site.ts +++ b/client-react/src/models/site/site.ts @@ -57,6 +57,7 @@ export enum ClientCertMode { Required = 'Required', Optional = 'Optional', OptionalInteractiveUser = 'OptionalInteractiveUser', + Ignore = 'Ignore', } export enum MinTlsVersion { @@ -139,7 +140,7 @@ export interface Site { clientAffinityEnabled: boolean; clientAffinityProxyEnabled: boolean; clientCertEnabled: boolean; - clientCertMode: ClientCertMode; + clientCertMode: string; clientCertExclusionPaths: string; hostNamesDisabled: boolean; domainVerificationIdentifiers: string; diff --git a/client-react/src/pages/app/app-settings/AppSettingsDataLoader.tsx b/client-react/src/pages/app/app-settings/AppSettingsDataLoader.tsx index 9237e68154..c7b8a529d4 100644 --- a/client-react/src/pages/app/app-settings/AppSettingsDataLoader.tsx +++ b/client-react/src/pages/app/app-settings/AppSettingsDataLoader.tsx @@ -159,7 +159,7 @@ const AppSettingsDataLoader: React.FC = props => { } const isLinux = isLinuxApp(site.data); - const windowsContainer = isWindowsContainer(site.data); + // Get stacks response if (!loadingFailed) { if (isFunctionApp(site.data)) { @@ -247,20 +247,10 @@ const AppSettingsDataLoader: React.FC = props => { setEditable(false); } - const sshEnabled = site.data.properties.sshEnabled; - const functionsRuntimeAdminIsolationEnabled: boolean = !!site.data.properties.functionsRuntimeAdminIsolationEnabled; - setInitialValues({ ...convertStateToForm({ // @note(krmitta): Manually over-writing since the api returns null when sshEnabled property is not set in the database but the default is true - site: { - ...site.data, - properties: { - ...site.data.properties, - sshEnabled: (isLinux || windowsContainer) && sshEnabled === null ? true : sshEnabled, - functionsRuntimeAdminIsolationEnabled: functionsRuntimeAdminIsolationEnabled, - }, - }, + site: site.data, config: webConfig.data, metadata: metadata.metadata.success ? metadata.data : null, connectionStrings: connectionStrings.metadata.success ? connectionStrings.data : null, diff --git a/client-react/src/pages/app/app-settings/AppSettingsFormData.ts b/client-react/src/pages/app/app-settings/AppSettingsFormData.ts index 2fb18e7075..a28219e4dc 100644 --- a/client-react/src/pages/app/app-settings/AppSettingsFormData.ts +++ b/client-react/src/pages/app/app-settings/AppSettingsFormData.ts @@ -14,7 +14,7 @@ import { } from './AppSettings.types'; import { sortBy, isEqual } from 'lodash-es'; import { ArmArray, ArmObj } from '../../../models/arm-obj'; -import { Site, PublishingCredentialPolicies, MinTlsVersion } from '../../../models/site/site'; +import { Site, PublishingCredentialPolicies, MinTlsVersion, ClientCertMode } from '../../../models/site/site'; import { SiteConfig, ArmAzureStorageMount, @@ -29,7 +29,7 @@ import { NameValuePair } from '../../../models/name-value-pair'; import StringUtils from '../../../utils/string'; import { CommonConstants } from '../../../utils/CommonConstants'; import { KeyValue } from '../../../models/portal-models'; -import { isFlexConsumption, isFunctionApp, isWindowsCode } from '../../../utils/arm-utils'; +import { isFlexConsumption, isFunctionApp, isLinuxApp, isWindowsCode, isWindowsContainer } from '../../../utils/arm-utils'; import { IconConstants } from '../../../utils/constants/IconConstants'; import { ThemeExtended } from '../../../theme/SemanticColorsExtended'; import { TFunction } from 'i18next'; @@ -93,7 +93,7 @@ export const convertStateToForm = (props: StateToFormParams): AppSettingsFormVal const formAppSetting = getFormAppSetting(appSettings, slotConfigNames); return { - site, + site: getCleanedSite(site), basicPublishingCredentialsPolicies: getFormBasicPublishingCredentialsPolicies(basicPublishingCredentialsPolicies), config: getCleanedConfig(config), appSettings: formAppSetting, @@ -113,6 +113,23 @@ export const convertStateToForm = (props: StateToFormParams): AppSettingsFormVal }; }; +export const getCleanedSite = (site: ArmObj) => { + let sshEnabled = site.properties.sshEnabled; + sshEnabled = (isLinuxApp(site) || isWindowsContainer(site)) && sshEnabled === null ? true : sshEnabled; + const functionsRuntimeAdminIsolationEnabled = !!site.properties.functionsRuntimeAdminIsolationEnabled; + const clientCertMode = site.properties.clientCertEnabled ? site.properties.clientCertMode : ClientCertMode.Ignore; + + return { + ...site, + properties: { + ...site.properties, + sshEnabled, + functionsRuntimeAdminIsolationEnabled, + clientCertMode, + }, + }; +}; + export const getCleanedConfig = (config: ArmObj) => { // If Remote Debugging Version is set to VS2015, but Remote Debugging is disabled, just change it to VS2017 to prevent the PUT from failing const hasRemoteDebuggingDisabledWithVS2015 = @@ -174,6 +191,13 @@ export const convertFormToState = ( oldSlotConfigNames: ArmObj ): ApiSetupReturn => { const site = { ...values.site }; + const { clientCertMode, ClientCertEnabled } = getClientCertValues( + initialValues.site.properties.clientCertMode, + values.site.properties.clientCertMode + ); + site.properties.clientCertMode = clientCertMode; + site.properties.clientCertEnabled = ClientCertEnabled; + const slotConfigNames = getStickySettings(values.appSettings, values.connectionStrings, values.azureStorageMounts, oldSlotConfigNames); const slotConfigNamesModified = isSlotConfigNamesModified(oldSlotConfigNames, slotConfigNames); @@ -239,6 +263,15 @@ export const getStorageMountAccessKey = (value: FormAzureStorageMounts) => { : accessKey; }; +export function getClientCertValues(initialClientCertMode: string, currentClientCertMode: string) { + const isClientCertModeIgnore = currentClientCertMode === ClientCertMode.Ignore; + + return { + clientCertMode: isClientCertModeIgnore ? initialClientCertMode : currentClientCertMode, + ClientCertEnabled: !isClientCertModeIgnore, + }; +} + export function getStickySettings( appSettings: FormAppSetting[], connectionStrings: FormConnectionString[], diff --git a/client-react/src/pages/app/app-settings/GeneralSettings/ClientCert/ClientCert.tsx b/client-react/src/pages/app/app-settings/GeneralSettings/ClientCert/ClientCert.tsx index 2bc1319e88..cf03ceb6b8 100644 --- a/client-react/src/pages/app/app-settings/GeneralSettings/ClientCert/ClientCert.tsx +++ b/client-react/src/pages/app/app-settings/GeneralSettings/ClientCert/ClientCert.tsx @@ -1,26 +1,20 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { settingsWrapper } from '../../AppSettingsForm'; import { Field, FormikProps } from 'formik'; -import RadioButtonNoFormik from '../../../../../components/form-controls/RadioButtonNoFormik'; import { useTranslation } from 'react-i18next'; import { PermissionsContext, SiteContext } from '../../Contexts'; import TextField from '../../../../../components/form-controls/TextField'; -import { Stack, PanelType, IChoiceGroupOption } from '@fluentui/react'; +import { Stack, PanelType, IChoiceGroupOption, MessageBarType } from '@fluentui/react'; import IconButton from '../../../../../components/IconButton/IconButton'; import EditClientExclusionPaths from './EditClientExclusionPaths'; import { AppSettingsFormValues } from '../../AppSettings.types'; import { ScenarioService } from '../../../../../utils/scenario-checker/scenario.service'; import { ScenarioIds } from '../../../../../utils/scenario-checker/scenario-ids'; import CustomPanel from '../../../../../components/CustomPanel/CustomPanel'; -import { ClientCertMode, Site } from '../../../../../models/site/site'; +import { ClientCertMode, MinTlsVersion, Site } from '../../../../../models/site/site'; import { ArmObj } from '../../../../../models/arm-obj'; - -enum CompositeClientCertMode { - Require = 'Require', - Allow = 'Allow', - Optional = 'Optional', - Ignore = 'Ignore', -} +import RadioButton from '../../../../../components/form-controls/RadioButton'; +import CustomBanner from '../../../../../components/CustomBanner/CustomBanner'; const ClientCert: React.FC> = props => { const { values, setFieldValue, initialValues } = props; @@ -29,53 +23,18 @@ const ClientCert: React.FC> = props => { const { app_write, editable, saving } = useContext(PermissionsContext); const disableAllControls = !app_write || !editable || saving; const [showPanel, setShowPanel] = useState(false); - - const onClientCertModeChange = (e: any, newValue: IChoiceGroupOption) => { - switch (newValue.key) { - case CompositeClientCertMode.Require: - setFieldValue('site.properties.clientCertEnabled', true); - setFieldValue('site.properties.clientCertMode', ClientCertMode.Required); - break; - case CompositeClientCertMode.Allow: - setFieldValue('site.properties.clientCertEnabled', true); - setFieldValue('site.properties.clientCertMode', ClientCertMode.Optional); - break; - case CompositeClientCertMode.Optional: - setFieldValue('site.properties.clientCertEnabled', true); - setFieldValue('site.properties.clientCertMode', ClientCertMode.OptionalInteractiveUser); - break; - case CompositeClientCertMode.Ignore: - setFieldValue('site.properties.clientCertEnabled', false); - break; - default: - setFieldValue('site.properties.clientCertEnabled', false); - break; - } - }; - - const getCompositeClientCertMode = (siteArm: ArmObj): CompositeClientCertMode => { - if (siteArm.properties.clientCertEnabled) { - return siteArm.properties.clientCertMode === ClientCertMode.Required - ? CompositeClientCertMode.Require - : siteArm.properties.clientCertMode === ClientCertMode.Optional - ? CompositeClientCertMode.Allow - : CompositeClientCertMode.Optional; - } - - return CompositeClientCertMode.Ignore; - }; + const [disableOptionalInteractiveUserOption, setDisableOptionalInteractiveUserOption] = useState(false); + const [clientCertWarningMessage, setClientCertWarningMessage] = useState(''); const getClientCertInfoBubbleMessage = (siteArm: ArmObj): string => { - const mode = getCompositeClientCertMode(siteArm); - - switch (mode) { - case CompositeClientCertMode.Require: + switch (siteArm.properties.clientCertMode) { + case ClientCertMode.Required: return t('clientCertificateModeRequiredInfoBubbleMessage'); - case CompositeClientCertMode.Allow: - return t('clientCertificateModeAllowInfoBubbleMessage'); - case CompositeClientCertMode.Optional: + case ClientCertMode.Optional: return t('clientCertificateModeOptionalInfoBubbleMessage'); - case CompositeClientCertMode.Ignore: + case ClientCertMode.OptionalInteractiveUser: + return t('clientCertificateModeOptionalInteractiveUserInfoBubbleMessage'); + case ClientCertMode.Ignore: return t('clientCertificateModeIgnoreInfoBubbleMessage'); default: return ''; @@ -84,6 +43,7 @@ const ClientCert: React.FC> = props => { const scenarioChecker = new ScenarioService(t); const clientCertEnabled = scenarioChecker.checkScenario(ScenarioIds.incomingClientCertEnabled, { site }); + const openClientExclusionPathPanel = () => { setShowPanel(true); }; @@ -95,38 +55,55 @@ const ClientCert: React.FC> = props => { setShowPanel(false); }; + useEffect(() => { + const http20EnabledOrTLSVersion12 = + values.config.properties.http20Enabled || values.config.properties.minTlsVersion === MinTlsVersion.tLS13; + const isClientCertModeOptionalInteractiveUser = values.site.properties.clientCertMode === ClientCertMode.OptionalInteractiveUser; + + setDisableOptionalInteractiveUserOption(http20EnabledOrTLSVersion12); + setClientCertWarningMessage(http20EnabledOrTLSVersion12 ? t('clientCertificateWarningMessage') : ''); + if (isClientCertModeOptionalInteractiveUser && http20EnabledOrTLSVersion12) { + setFieldValue('site.properties.clientCertMode', ClientCertMode.Ignore); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [values.config.properties.http20Enabled, values.config.properties.minTlsVersion, values.site.properties.clientCertMode]); + return scenarioChecker.checkScenario(ScenarioIds.incomingClientCertSupported, { site }).status !== 'disabled' ? ( <>

{t('incomingClientCertificates')}

- + )} + > = props => { id={`edit-client-cert-exclusion-paths`} ariaLabel={t('editCertificateExlusionPaths')} title={t('editCertificateExlusionPaths')} - disabled={disableAllControls || !values.site.properties.clientCertEnabled} + disabled={disableAllControls || values.site.properties.clientCertMode === ClientCertMode.Ignore} onClick={openClientExclusionPathPanel} /> diff --git a/client-react/src/pages/app/app-settings/GeneralSettings/Platform.tsx b/client-react/src/pages/app/app-settings/GeneralSettings/Platform.tsx index 43fb3e0efd..810dd23f97 100644 --- a/client-react/src/pages/app/app-settings/GeneralSettings/Platform.tsx +++ b/client-react/src/pages/app/app-settings/GeneralSettings/Platform.tsx @@ -8,7 +8,7 @@ import { ScenarioService } from '../../../../utils/scenario-checker/scenario.ser import { AppSettingsFormValues } from '../AppSettings.types'; import { PermissionsContext, SiteContext } from '../Contexts'; import { Links } from '../../../../utils/FwLinks'; -import { IPMode, MinTlsVersion, SslState, VnetPrivatePortsCount } from '../../../../models/site/site'; +import { ClientCertMode, IPMode, MinTlsVersion, SslState, VnetPrivatePortsCount } from '../../../../models/site/site'; import CustomBanner from '../../../../components/CustomBanner/CustomBanner'; import { IDropdownOption, MessageBar, MessageBarType, mergeStyles } from '@fluentui/react'; import { CommonConstants, ScmHosts } from '../../../../utils/CommonConstants'; @@ -111,12 +111,8 @@ const Platform: React.FC> = props => { [setFieldValue] ); - const onHttp20EnabledChange = (event: React.FormEvent, option: { key: boolean }) => { + const onHttp20EnabledChange = (_, option: { key: boolean }) => { props.setFieldValue('config.properties.http20ProxyFlag', 0); - if (option.key) { - props.setFieldValue('site.properties.clientCertEnabled', false); - } - props.setFieldValue('config.properties.http20Enabled', option.key); }; diff --git a/client/src/app/shared/models/portal-resources.ts b/client/src/app/shared/models/portal-resources.ts index eaed2162c9..7eb6da0cb6 100644 --- a/client/src/app/shared/models/portal-resources.ts +++ b/client/src/app/shared/models/portal-resources.ts @@ -1686,14 +1686,16 @@ export class PortalResources { public static incomingClientCertificates = 'incomingClientCertificates'; public static requireIncomingClientCertificates = 'requireIncomingClientCertificates'; public static clientCertificateMode = 'clientCertificateMode'; - public static clientCertificateModeRequire = 'clientCertificateModeRequire'; - public static clientCertificateModeAllow = 'clientCertificateModeAllow'; + public static clientCertificateModeRequired = 'clientCertificateModeRequired'; public static clientCertificateModeOptional = 'clientCertificateModeOptional'; + public static clientCertificateModeOptionalInteractiveUser = 'clientCertificateModeOptionalInteractiveUser'; public static clientCertificateModeIgnore = 'clientCertificateModeIgnore'; public static clientCertificateModeRequiredInfoBubbleMessage = 'clientCertificateModeRequiredInfoBubbleMessage'; - public static clientCertificateModeAllowInfoBubbleMessage = 'clientCertificateModeAllowInfoBubbleMessage'; public static clientCertificateModeOptionalInfoBubbleMessage = 'clientCertificateModeOptionalInfoBubbleMessage'; + public static clientCertificateModeOptionalInteractiveUserInfoBubbleMessage = + 'clientCertificateModeOptionalInteractiveUserInfoBubbleMessage'; public static clientCertificateModeIgnoreInfoBubbleMessage = 'clientCertificateModeIgnoreInfoBubbleMessage'; + public static clientCertificateWarningMessage = 'clientCertificateWarningMessage'; public static certificateExlusionPaths = 'certificateExlusionPaths'; public static editCertificateExlusionPaths = 'editCertificateExlusionPaths'; public static noExclusionRulesDefined = 'noExclusionRulesDefined'; diff --git a/server/Resources/Resources.resx b/server/Resources/Resources.resx index 4ec59218d3..83caacbf9c 100644 --- a/server/Resources/Resources.resx +++ b/server/Resources/Resources.resx @@ -5246,30 +5246,33 @@ Set to "External URL" to use an API definition that is hosted elsewhere. Client certificate mode - + Require - - Allow - Optional + + Optional Interactive User + Ignore All requests must be authenticated through a client certificate. - + Clients will be prompted for a certificate, if no certificate is provided fallback to SSO or other means of authentication. Unauthenticated requests will be blocked. - + Clients will not be prompted for a certificate by default. Unless the request can be authenticated through other means (like SSO), it will be blocked. No client authentication is required. Unauthenticated requests will not be blocked. + + Client certificate mode "Optional Interactive User" and client certificate exclusion path regardless of client certificate mode are not compatible with Http version 2.0 or minimum inbound TLS Version 1.3 + Certificate exclusion paths