Skip to content

Commit

Permalink
Add control for Enabling/Disabling SSH on Linux web apps (#7426)
Browse files Browse the repository at this point in the history
* add radio button

* disable feature added

* add ssh info bubble

* overwrite and update

---------

Co-authored-by: Krrish Mittal <[email protected]>
  • Loading branch information
takyyon and Krrish Mittal authored Oct 11, 2023
1 parent e9f1cb0 commit 0d2573f
Show file tree
Hide file tree
Showing 14 changed files with 108 additions and 28 deletions.
1 change: 1 addition & 0 deletions client-react/src/models/site/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export interface Site {
identity: MsiIdentity;
vnetImagePullEnabled: boolean;
keyVaultReferenceIdentity: string;
sshEnabled?: boolean | null;
}

export interface HostNameSslState {
Expand Down
5 changes: 5 additions & 0 deletions client-react/src/models/stacks/app-stacks.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
interface SupportedFeatures {
disableSSH?: boolean;
}

export interface AppStack<T> {
displayText: string;
value: string;
Expand Down Expand Up @@ -40,4 +44,5 @@ export interface CommonSettings {
isAutoUpdate?: boolean;
isEarlyAccess?: boolean;
gitHubActionSettings: GitHubActionSettings;
supportedFeatures?: SupportedFeatures;
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@ const AppSettingsDataLoader: React.FC<AppSettingsDataLoaderProps> = props => {
loadingFailed = loadingFailed || armCallFailed(azureStorageMounts, true);
}

const isLinux = isLinuxApp(site.data);
// Get stacks response
if (!loadingFailed) {
const isLinux = isLinuxApp(site.data);
if (isFunctionApp(site.data)) {
const stacksResponse = await RuntimeStackService.getFunctionAppConfigurationStacks(isLinux ? AppStackOs.linux : AppStackOs.windows);
if (stacksResponse.metadata.status && !!stacksResponse.data.value) {
Expand Down Expand Up @@ -232,9 +232,13 @@ const AppSettingsDataLoader: React.FC<AppSettingsDataLoaderProps> = props => {
if (site.data.properties.targetSwapSlot) {
setEditable(false);
}

const sshEnabled = site.data.properties.sshEnabled;

setInitialValues({
...convertStateToForm({
site: site.data,
// @note(krmitta): Manually over-writing since the api returns null when ssh is disabled but there isn't a value in the database
site: { ...site.data, properties: { ...site.data.properties, sshEnabled: isLinux && sshEnabled === null ? false : sshEnabled } },
config: webConfig.data,
metadata: metadata.metadata.success ? metadata.data : null,
connectionStrings: connectionStrings.metadata.success ? connectionStrings.data : null,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import i18next from 'i18next';
import { WebAppStack } from '../../../../../models/stacks/web-app-stacks';
import { WebAppRuntimeSettings, WebAppStack } from '../../../../../models/stacks/web-app-stacks';
import { getMinorVersionText } from '../../../../../utils/stacks-utils';

export const LINUXJAVASTACKKEY = 'java';
Expand All @@ -11,6 +11,7 @@ interface VersionDetails {
majorVersionRuntime: string;
minorVersionName: string;
minorVersionRuntime: string;
data?: WebAppRuntimeSettings;
}

export const getRuntimeStacks = (builtInStacks: WebAppStack[]) => {
Expand Down Expand Up @@ -58,13 +59,14 @@ export const getMinorVersions = (builtInStacks: WebAppStack[], stack: string, ma
};

export const getVersionDetails = (builtInStacks: WebAppStack[], version: string): VersionDetails => {
let versionDetails = {
let versionDetails: VersionDetails = {
runtimeStackName: '',
majorVersionName: '',
majorVersionRuntime: '',
minorVersionName: '',
minorVersionRuntime: '',
};

if (!!builtInStacks && !!version) {
builtInStacks.forEach(stack => {
stack.majorVersions.forEach(stackMajorVersion => {
Expand All @@ -80,6 +82,7 @@ export const getVersionDetails = (builtInStacks: WebAppStack[], version: string)
majorVersionRuntime: stackMajorVersion.value,
minorVersionName: stackMinorVersion.value,
minorVersionRuntime: setting ? setting.runtimeVersion : stackMinorVersion.value,
data: setting,
};
}
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Field, FormikProps } from 'formik';
import React, { useContext, useState, useEffect } from 'react';
import React, { useContext, useState, useEffect, useMemo } from 'react';
import Dropdown from '../../../../../components/form-controls/DropDown';
import { AppSettingsFormValues } from '../../AppSettings.types';
import { PermissionsContext, WebAppStacksContext } from '../../Contexts';
import { PermissionsContext } from '../../Contexts';
import TextField from '../../../../../components/form-controls/TextField';
import { useTranslation } from 'react-i18next';
import { ScenarioService } from '../../../../../utils/scenario-checker/scenario.service';
Expand All @@ -13,7 +13,6 @@ import {
getRuntimeStacks,
getSelectedRuntimeStack,
getSelectedMajorVersion,
getVersionDetails,
getSelectedMinorVersion,
getMajorVersions,
getMinorVersions,
Expand All @@ -29,6 +28,7 @@ import {
isStackVersionEndOfLife,
} from '../../../../../utils/stacks-utils';
import { SiteStateContext } from '../../../../../SiteState';
import useStacks from '../../Hooks/useStacks';

type PropsType = FormikProps<AppSettingsFormValues>;

Expand All @@ -37,10 +37,10 @@ const LinuxStacks: React.FC<PropsType> = props => {
const { site } = values;
const { app_write, editable, saving } = useContext(PermissionsContext);
const disableAllControls = !app_write || !editable || saving;
let supportedStacks = useContext(WebAppStacksContext);
const siteStateContext = useContext(SiteStateContext);

const runtimeOptions = getRuntimeStacks(supportedStacks);
const { webAppStacks, initialStackVersionDetails } = useStacks();
const siteStateContext = useContext(SiteStateContext);
const runtimeOptions = getRuntimeStacks(webAppStacks);
const { t } = useTranslation();
const scenarioService = new ScenarioService(t);

Expand All @@ -49,23 +49,26 @@ const LinuxStacks: React.FC<PropsType> = props => {
const [earlyAccessInfoVisible, setEarlyAccessInfoVisible] = useState(false);
const [eolStackDate, setEolStackDate] = useState<string | null | undefined>(undefined);

const initialVersionDetails = getVersionDetails(supportedStacks, initialValues.config.properties.linuxFxVersion);
supportedStacks = filterDeprecatedWebAppStack(
supportedStacks,
initialVersionDetails.runtimeStackName,
initialVersionDetails.minorVersionRuntime
const filterredWebAppStacks = useMemo(
() =>
filterDeprecatedWebAppStack(
webAppStacks,
initialStackVersionDetails.runtimeStackName,
initialStackVersionDetails.minorVersionRuntime
),
[webAppStacks, initialStackVersionDetails.runtimeStackName, initialStackVersionDetails.minorVersionRuntime]
);

const isRuntimeStackDirty = (): boolean =>
getRuntimeStack(values.config.properties.linuxFxVersion) !== getRuntimeStack(initialValues.config.properties.linuxFxVersion);

const isMajorVersionDirty = (): boolean =>
(majorVersionRuntime || '').toLowerCase() !== initialVersionDetails.majorVersionRuntime.toLowerCase();
(majorVersionRuntime || '').toLowerCase() !== initialStackVersionDetails.majorVersionRuntime.toLowerCase();

const isMinorVersionDirty = (): boolean => {
if (runtimeStack) {
const minorVersion = getSelectedMinorVersion(supportedStacks, runtimeStack, values.config.properties.linuxFxVersion);
return (minorVersion || '').toLowerCase() !== initialVersionDetails.minorVersionRuntime.toLowerCase();
const minorVersion = getSelectedMinorVersion(filterredWebAppStacks, runtimeStack, values.config.properties.linuxFxVersion);
return (minorVersion || '').toLowerCase() !== initialStackVersionDetails.minorVersionRuntime.toLowerCase();
} else {
return false;
}
Expand All @@ -74,11 +77,11 @@ const LinuxStacks: React.FC<PropsType> = props => {
const onRuntimeStackChange = (newRuntimeStack: string) => {
setRuntimeStack(newRuntimeStack);
if (newRuntimeStack !== LINUXJAVASTACKKEY) {
const majorVersions = getMajorVersions(supportedStacks, newRuntimeStack);
const majorVersions = getMajorVersions(filterredWebAppStacks, newRuntimeStack);
if (majorVersions.length > 0) {
const majVer = majorVersions[0];
setMajorVersionRuntime(majVer.key as string);
const minorVersions = getMinorVersions(supportedStacks, newRuntimeStack, majVer.key as string, t);
const minorVersions = getMinorVersions(filterredWebAppStacks, newRuntimeStack, majVer.key as string, t);
if (minorVersions.length > 0) {
setFieldValue('config.properties.linuxFxVersion', minorVersions[0].key);
}
Expand All @@ -88,7 +91,7 @@ const LinuxStacks: React.FC<PropsType> = props => {

const onMajorVersionChange = (newMajorVersion: string) => {
if (runtimeStack) {
const minorVersions = getMinorVersions(supportedStacks, runtimeStack, newMajorVersion, t);
const minorVersions = getMinorVersions(filterredWebAppStacks, runtimeStack, newMajorVersion, t);
setMajorVersionRuntime(newMajorVersion);
if (minorVersions.length > 0) {
setFieldValue('config.properties.linuxFxVersion', minorVersions[0].key);
Expand All @@ -97,22 +100,22 @@ const LinuxStacks: React.FC<PropsType> = props => {
};

const getRuntimeStack = (linuxFxVersion: string) => {
return isJavaStackSelected(supportedStacks, linuxFxVersion)
return isJavaStackSelected(filterredWebAppStacks, linuxFxVersion)
? LINUXJAVASTACKKEY
: getSelectedRuntimeStack(supportedStacks, linuxFxVersion);
: getSelectedRuntimeStack(filterredWebAppStacks, linuxFxVersion);
};

const setRuntimeStackAndMajorVersion = () => {
setRuntimeStack(getRuntimeStack(values.config.properties.linuxFxVersion));
setMajorVersionRuntime(getSelectedMajorVersion(supportedStacks, values.config.properties.linuxFxVersion));
setMajorVersionRuntime(getSelectedMajorVersion(filterredWebAppStacks, values.config.properties.linuxFxVersion));
};

const setEolDate = () => {
setEarlyAccessInfoVisible(false);
setEolStackDate(undefined);

if (runtimeStack && majorVersionRuntime) {
const minorVersions = getMinorVersions(supportedStacks, runtimeStack, majorVersionRuntime, t);
const minorVersions = getMinorVersions(filterredWebAppStacks, runtimeStack, majorVersionRuntime, t);
const selectedMinorVersion = values.config.properties.linuxFxVersion.toLowerCase();
for (const minorVersion of minorVersions) {
if (minorVersion.key === selectedMinorVersion && minorVersion.data) {
Expand Down Expand Up @@ -160,7 +163,7 @@ const LinuxStacks: React.FC<PropsType> = props => {
selectedKey={majorVersionRuntime || ''}
dirty={isMajorVersionDirty()}
onChange={(e, newVal) => onMajorVersionChange(newVal.key)}
options={getMajorVersions(supportedStacks, runtimeStack)}
options={getMajorVersions(filterredWebAppStacks, runtimeStack)}
disabled={disableAllControls}
label={t('majorVersion')}
id="linux-fx-version-major-version"
Expand All @@ -174,7 +177,7 @@ const LinuxStacks: React.FC<PropsType> = props => {
disabled={disableAllControls}
label={t('minorVersion')}
id="linux-fx-version-minor-version"
options={getMinorVersions(supportedStacks, runtimeStack, majorVersionRuntime, t)}
options={getMinorVersions(filterredWebAppStacks, runtimeStack, majorVersionRuntime, t)}
{...getEarlyStackMessageParameters(earlyAccessInfoVisible, t)}
/>
{checkAndGetStackEOLOrDeprecatedBanner(t, values.config.properties.linuxFxVersion, eolStackDate)}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Field, FormikProps } from 'formik';
import React, { useCallback, useContext } from 'react';
import React, { useCallback, useContext, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import Dropdown from '../../../../components/form-controls/DropDown';
import RadioButton from '../../../../components/form-controls/RadioButton';
Expand All @@ -14,17 +14,23 @@ import { MessageBarType } from '@fluentui/react';
import { ScmHosts } from '../../../../utils/CommonConstants';
import MinTLSCipherSuiteSelector from '../../../../components/CipherSuite/MinTLSCipherSuiteSelector';
import TextFieldNoFormik from '../../../../components/form-controls/TextFieldNoFormik';
import useStacks from '../Hooks/useStacks';

const Platform: React.FC<FormikProps<AppSettingsFormValues>> = props => {
const site = useContext(SiteContext);
const { t } = useTranslation();
const { values, initialValues, setFieldValue } = props;
const scenarioChecker = new ScenarioService(t);
const { app_write, editable, saving } = useContext(PermissionsContext);

// @note(krmitta): Only this for linux apps for now.
const { initialStackVersionDetails: stackVersionDetails } = useStacks(values?.config.properties.linuxFxVersion);

const disableAllControls = !app_write || !editable || saving;
const platformOptionEnable = scenarioChecker.checkScenario(ScenarioIds.enablePlatform64, { site });
const websocketsEnable = scenarioChecker.checkScenario(ScenarioIds.webSocketsEnabled, { site });
const alwaysOnEnable = scenarioChecker.checkScenario(ScenarioIds.enableAlwaysOn, { site });
const sshControlEnabled = useMemo(() => stackVersionDetails.data?.supportedFeatures?.disableSSH, [stackVersionDetails]);

const showHttpsOnlyInfo = (): boolean => {
const siteProperties = values.site.properties;
Expand Down Expand Up @@ -256,6 +262,27 @@ const Platform: React.FC<FormikProps<AppSettingsFormValues>> = props => {
]}
/>
)}
{scenarioChecker.checkScenario(ScenarioIds.sshEnabledSupported, { site }).status == 'enabled' && (
<Field
name="site.properties.sshEnabled"
dirty={values.site.properties.sshEnabled !== initialValues.site.properties.sshEnabled}
component={RadioButton}
label={t('feature_sshName')}
id="app-settings-ssh-enabled"
disabled={disableAllControls || !sshControlEnabled}
infoBubbleMessage={sshControlEnabled ? '' : t('sshDisabledInfoBubbleMessage')}
options={[
{
key: true,
text: t('on'),
},
{
key: false,
text: t('off'),
},
]}
/>
)}
{scenarioChecker.checkScenario(ScenarioIds.alwaysOnSupported, { site }).status !== 'disabled' && (
<Field
name="config.properties.alwaysOn"
Expand Down
12 changes: 12 additions & 0 deletions client-react/src/pages/app/app-settings/Hooks/useStacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { useContext, useMemo } from 'react';
import { WebAppStacksContext } from '../Contexts';
import { getVersionDetails } from '../GeneralSettings/LinuxStacks/LinuxStacks.data';

const useStacks = (stackVersion: string = '') => {
const webAppStacks = useContext(WebAppStacksContext);
const initialStackVersionDetails = useMemo(() => getVersionDetails(webAppStacks, stackVersion), [webAppStacks, stackVersion]);

return { initialStackVersionDetails, webAppStacks };
};

export default useStacks;
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,11 @@ export class FunctionAppEnvironment extends Environment {
id: ScenarioIds.ftpSource,
runCheck: () => ({ status: 'disabled' }),
};

this.scenarioChecks[ScenarioIds.sshEnabledSupported] = {
id: ScenarioIds.sshEnabledSupported,
runCheck: () => ({ status: 'disabled' }),
};
}

public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ export class KubeApp extends Environment {
return { status: 'disabled' };
},
};

this.scenarioChecks[ScenarioIds.sshEnabledSupported] = {
id: ScenarioIds.sshEnabledSupported,
runCheck: () => ({ status: 'disabled' }),
};
}

public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ export class LinuxSiteEnvironment extends Environment {
};
},
};

this.scenarioChecks[ScenarioIds.sshEnabledSupported] = {
id: ScenarioIds.sshEnabledSupported,
runCheck: () => ({ status: 'enabled' }),
};
}

public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
Expand Down
1 change: 1 addition & 0 deletions client-react/src/utils/scenario-checker/scenario-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,4 +119,5 @@ export class ScenarioIds {
public static readonly enableCustomErrorPagesOverlay = 'enableCustomErrorPagesOverlay';
public static readonly basicAuthPublishingCreds = 'basicAuthPublishingCreds';
public static readonly vnetPrivatePortsCount = 'vnetPrivatePortsCount';
public static readonly sshEnabledSupported = 'sshEnabledSupported';
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ export class WorkflowAppEnvironment extends FunctionAppEnvironment {
id: ScenarioIds.vnetPrivatePortsCount,
runCheck: () => ({ status: 'enabled' }),
};

this.scenarioChecks[ScenarioIds.sshEnabledSupported] = {
id: ScenarioIds.sshEnabledSupported,
runCheck: () => ({ status: 'disabled' }),
};
}

public isCurrentEnvironment(input?: ScenarioCheckInput): boolean {
Expand Down
1 change: 1 addition & 0 deletions client/src/app/shared/models/portal-resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2576,4 +2576,5 @@ export class PortalResources {
public static authenticationSettingsIdentity = 'authenticationSettingsIdentity';
public static authenticationSettingsIdentityPermissionsError = 'authenticationSettingsIdentityPermissionsError';
public static authenticationSettingsIdentityPermissionsLinkAriaLabel = 'authenticationSettingsIdentityPermissionsLinkAriaLabel';
public static sshDisabledInfoBubbleMessage = 'sshDisabledInfoBubbleMessage';
}
3 changes: 3 additions & 0 deletions server/Resources/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -7870,4 +7870,7 @@ Set to "External URL" to use an API definition that is hosted elsewhere.</value>
<data name="authenticationSettingsIdentityPermissionsLinkAriaLabel" xml:space="preserve">
<value>Click here to learn more about how to manage federated identity credentials on a user-assigned identity.</value>
</data>
<data name="sshDisabledInfoBubbleMessage" xml:space="preserve">
<value>SSH isn't supported for your selected stack version.</value>
</data>
</root>

0 comments on commit 0d2573f

Please sign in to comment.