Skip to content

Commit

Permalink
disable optional interactive user for http 20 or min tls version 1.3
Browse files Browse the repository at this point in the history
  • Loading branch information
Qi Liu committed Nov 22, 2024
1 parent 003c517 commit ff8147c
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 98 deletions.
3 changes: 2 additions & 1 deletion client-react/src/models/site/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export enum ClientCertMode {
Required = 'Required',
Optional = 'Optional',
OptionalInteractiveUser = 'OptionalInteractiveUser',
Ignore = 'Ignore',
}

export enum MinTlsVersion {
Expand Down Expand Up @@ -139,7 +140,7 @@ export interface Site {
clientAffinityEnabled: boolean;
clientAffinityProxyEnabled: boolean;
clientCertEnabled: boolean;
clientCertMode: ClientCertMode;
clientCertMode: string;
clientCertExclusionPaths: string;
hostNamesDisabled: boolean;
domainVerificationIdentifiers: string;
Expand Down
14 changes: 2 additions & 12 deletions client-react/src/pages/app/app-settings/AppSettingsDataLoader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ const AppSettingsDataLoader: React.FC<AppSettingsDataLoaderProps> = props => {
}

const isLinux = isLinuxApp(site.data);
const windowsContainer = isWindowsContainer(site.data);

// Get stacks response
if (!loadingFailed) {
if (isFunctionApp(site.data)) {
Expand Down Expand Up @@ -247,20 +247,10 @@ const AppSettingsDataLoader: React.FC<AppSettingsDataLoaderProps> = 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,
Expand Down
39 changes: 36 additions & 3 deletions client-react/src/pages/app/app-settings/AppSettingsFormData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -113,6 +113,23 @@ export const convertStateToForm = (props: StateToFormParams): AppSettingsFormVal
};
};

export const getCleanedSite = (site: ArmObj<Site>) => {
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<SiteConfig>) => {
// 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 =
Expand Down Expand Up @@ -174,6 +191,13 @@ export const convertFormToState = (
oldSlotConfigNames: ArmObj<SlotConfigNames>
): 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);

Expand Down Expand Up @@ -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[],
Expand Down
Original file line number Diff line number Diff line change
@@ -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<FormikProps<AppSettingsFormValues>> = props => {
const { values, setFieldValue, initialValues } = props;
Expand All @@ -29,53 +23,18 @@ const ClientCert: React.FC<FormikProps<AppSettingsFormValues>> = 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<Site>): 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<Site>): 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 '';
Expand All @@ -84,6 +43,7 @@ const ClientCert: React.FC<FormikProps<AppSettingsFormValues>> = props => {

const scenarioChecker = new ScenarioService(t);
const clientCertEnabled = scenarioChecker.checkScenario(ScenarioIds.incomingClientCertEnabled, { site });

const openClientExclusionPathPanel = () => {
setShowPanel(true);
};
Expand All @@ -95,38 +55,55 @@ const ClientCert: React.FC<FormikProps<AppSettingsFormValues>> = 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' ? (
<>
<h3>{t('incomingClientCertificates')}</h3>
<div className={settingsWrapper}>
<RadioButtonNoFormik
dirty={getCompositeClientCertMode(values.site) !== getCompositeClientCertMode(initialValues.site)}
{clientCertWarningMessage && (
<CustomBanner id="clinet-cert-warning" message={clientCertWarningMessage} type={MessageBarType.warning} undocked={true} />
)}
<Field
name={'site.properties.clientCertMode'}
component={RadioButton}
dirty={values.site.properties.clientCertMode !== initialValues.site.properties.clientCertMode}
label={t('clientCertificateMode')}
id="incoming-client-certificate-mode"
ariaLabelledBy={`incoming-client-certificate-mode-label`}
disabled={disableAllControls || clientCertEnabled.status === 'disabled' || values.config.properties.http20Enabled}
disabled={disableAllControls || clientCertEnabled.status === 'disabled'}
upsellMessage={clientCertEnabled.status === 'disabled' ? clientCertEnabled.data : ''}
selectedKey={getCompositeClientCertMode(values.site)}
infoBubbleMessage={getClientCertInfoBubbleMessage(values.site)}
options={[
{
key: CompositeClientCertMode.Require,
text: t('clientCertificateModeRequire'),
key: ClientCertMode.Required,
text: t('clientCertificateModeRequired'),
},
{
key: CompositeClientCertMode.Allow,
text: t('clientCertificateModeAllow'),
key: ClientCertMode.Optional,
text: t('clientCertificateModeOptional'),
},
{
key: CompositeClientCertMode.Optional,
text: t('clientCertificateModeOptional'),
key: ClientCertMode.OptionalInteractiveUser,
text: t('clientCertificateModeOptionalInteractiveUser'),
disabled: disableOptionalInteractiveUserOption,
},
{
key: CompositeClientCertMode.Ignore,
key: ClientCertMode.Ignore,
text: t('clientCertificateModeIgnore'),
},
]}
onChange={onClientCertModeChange}
/>
<Stack horizontal>
<Field
Expand All @@ -150,7 +127,7 @@ const ClientCert: React.FC<FormikProps<AppSettingsFormValues>> = 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}
/>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -111,12 +111,8 @@ const Platform: React.FC<FormikProps<AppSettingsFormValues>> = props => {
[setFieldValue]
);

const onHttp20EnabledChange = (event: React.FormEvent<HTMLDivElement>, 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);
};

Expand Down
8 changes: 5 additions & 3 deletions client/src/app/shared/models/portal-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
15 changes: 9 additions & 6 deletions server/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -5246,30 +5246,33 @@ Set to "External URL" to use an API definition that is hosted elsewhere.</value>
<data name="clientCertificateMode" xml:space="preserve">
<value>Client certificate mode</value>
</data>
<data name="clientCertificateModeRequire" xml:space="preserve">
<data name="clientCertificateModeRequired" xml:space="preserve">
<value>Require</value>
</data>
<data name="clientCertificateModeAllow" xml:space="preserve">
<value>Allow</value>
</data>
<data name="clientCertificateModeOptional" xml:space="preserve">
<value>Optional</value>
</data>
<data name="clientCertificateModeOptionalInteractiveUser" xml:space="preserve">
<value>Optional Interactive User</value>
</data>
<data name="clientCertificateModeIgnore" xml:space="preserve">
<value>Ignore</value>
</data>
<data name="clientCertificateModeRequiredInfoBubbleMessage" xml:space="preserve">
<value>All requests must be authenticated through a client certificate.</value>
</data>
<data name="clientCertificateModeAllowInfoBubbleMessage" xml:space="preserve">
<data name="clientCertificateModeOptionalInfoBubbleMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="clientCertificateModeOptionalInfoBubbleMessage" xml:space="preserve">
<data name="clientCertificateModeOptionalInteractiveUserInfoBubbleMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="clientCertificateModeIgnoreInfoBubbleMessage" xml:space="preserve">
<value>No client authentication is required. Unauthenticated requests will not be blocked.</value>
</data>
<data name="clientCertificateWarningMessage" xml:space="preserve">
<value>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</value>
</data>
<data name="certificateExlusionPaths" xml:space="preserve">
<value>Certificate exclusion paths</value>
</data>
Expand Down

0 comments on commit ff8147c

Please sign in to comment.