Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AB#29764273 [Configuration]Disable optional interactive user for http 20 or min tls version 1.3 #7917

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 http20EnabledOrMinTLSVersion13 =
values.config.properties.http20Enabled || values.config.properties.minTlsVersion === MinTlsVersion.tLS13;
const isClientCertModeOptionalInteractiveUser = values.site.properties.clientCertMode === ClientCertMode.OptionalInteractiveUser;

setDisableOptionalInteractiveUserOption(http20EnabledOrMinTLSVersion13);
setClientCertWarningMessage(http20EnabledOrMinTLSVersion13 ? t('clientCertificateWarningMessage') : '');
if (isClientCertModeOptionalInteractiveUser && http20EnabledOrMinTLSVersion13) {
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
Loading