diff --git a/client/my-sites/site-settings/allow-list.jsx b/client/my-sites/site-settings/allow-list.jsx new file mode 100644 index 0000000000000..b665ec4035729 --- /dev/null +++ b/client/my-sites/site-settings/allow-list.jsx @@ -0,0 +1,165 @@ +import { Button, Card } from '@automattic/components'; +import { ToggleControl } from '@wordpress/components'; +import { localize } from 'i18n-calypso'; +import { includes, some } from 'lodash'; +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import FormFieldset from 'calypso/components/forms/form-fieldset'; +import FormLegend from 'calypso/components/forms/form-legend'; +import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation'; +import FormTextarea from 'calypso/components/forms/form-textarea'; + +class AllowList extends Component { + static propTypes = { + fields: PropTypes.object.isRequired, + isRequestingSettings: PropTypes.bool, + isSavingSettings: PropTypes.bool, + onChangeField: PropTypes.func.isRequired, + setFieldValue: PropTypes.func.isRequired, + }; + + static defaultProps = { + fields: {}, + isRequestingSettings: true, + isSavingSettings: false, + }; + + togglingAllowListSupported = () => { + return this.props.settings.jetpack_waf_ip_allow_list_enabled !== undefined; + }; + + showAllowList = () => { + return ( + ! this.togglingAllowListSupported() || this.props.fields.jetpack_waf_ip_allow_list_enabled + ); + }; + + handleAddToAllowedList = () => { + const { setFieldValue } = this.props; + let allowedIps = this.getProtectAllowedIps().trimEnd(); + + if ( allowedIps.length ) { + allowedIps += '\n'; + } + + setFieldValue( 'jetpack_waf_ip_allow_list', allowedIps + this.getIpAddress() ); + }; + + getIpAddress() { + if ( window.app && window.app.clientIp ) { + return window.app.clientIp; + } + + return null; + } + + getProtectAllowedIps() { + const { jetpack_waf_ip_allow_list } = this.props.fields; + return jetpack_waf_ip_allow_list || ''; + } + + isIpAddressAllowed() { + const ipAddress = this.getIpAddress(); + if ( ! ipAddress ) { + return false; + } + + const allowedIps = this.getProtectAllowedIps().split( '\n' ); + + return ( + includes( allowedIps, ipAddress ) || + some( allowedIps, ( entry ) => { + if ( entry.indexOf( '-' ) < 0 ) { + return false; + } + + const range = entry.split( '-' ).map( ( ip ) => ip.trim() ); + return includes( range, ipAddress ); + } ) + ); + } + + disableForm() { + return this.props.isRequestingSettings || this.props.isSavingSettings; + } + + render() { + const { translate } = this.props; + const ipAddress = this.getIpAddress(); + const isIpAllowed = this.isIpAddressAllowed(); + + return ( +
+ + + { this.togglingAllowListSupported() ? ( + + ) : ( + { translate( 'Always allow specific IP addresses' ) } + ) }{ ' ' } + + { translate( + "IP addresses added to this list will never be blocked by Jetpack's security features." + ) } + + { this.showAllowList() && ( +
+ + + { translate( + 'IPv4 and IPv6 are acceptable. ' + + 'To specify a range, enter the low value and high value separated by a dash. ' + + 'Example: 12.12.12.1-12.12.12.100' + ) } + +

+ { translate( 'Your current IP address: {{strong}}%(IP)s{{/strong}}{{br/}}', { + args: { + IP: ipAddress || translate( 'Unknown IP address' ), + }, + components: { + strong: , + br:
, + }, + } ) } + + { ipAddress && ( + + ) } +

+
+ ) } +
+
+
+ ); + } +} + +export default localize( AllowList ); diff --git a/client/my-sites/site-settings/firewall.jsx b/client/my-sites/site-settings/firewall.jsx new file mode 100644 index 0000000000000..b80aa9a94229d --- /dev/null +++ b/client/my-sites/site-settings/firewall.jsx @@ -0,0 +1,269 @@ +import { PRODUCT_JETPACK_SCAN, WPCOM_FEATURES_SCAN } from '@automattic/calypso-products'; +import { Card } from '@automattic/components'; +import { ToggleControl } from '@wordpress/components'; +import { localize } from 'i18n-calypso'; +import moment from 'moment'; +import PropTypes from 'prop-types'; +import { Component } from 'react'; +import { connect } from 'react-redux'; +import { UpsellNudge } from 'calypso/blocks/upsell-nudge'; +import QueryJetpackConnection from 'calypso/components/data/query-jetpack-connection'; +import FormFieldset from 'calypso/components/forms/form-fieldset'; +import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation'; +import FormTextarea from 'calypso/components/forms/form-textarea'; +import { activateModule } from 'calypso/state/jetpack/modules/actions'; +import getJetpackModule from 'calypso/state/selectors/get-jetpack-module'; +import isFetchingJetpackModules from 'calypso/state/selectors/is-fetching-jetpack-modules'; +import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-active'; +import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode'; +import isJetpackSiteInDevelopmentMode from 'calypso/state/selectors/is-jetpack-site-in-development-mode'; +import siteHasFeature from 'calypso/state/selectors/site-has-feature'; +import { getSelectedSiteId, getSelectedSiteSlug } from 'calypso/state/ui/selectors'; +import AllowList from './allow-list'; +import Protect from './protect'; +import SettingsSectionHeader from './settings-section-header'; + +class Firewall extends Component { + static propTypes = { + fields: PropTypes.object.isRequired, + handleSubmitForm: PropTypes.func.isRequired, + isAtomic: PropTypes.bool.isRequired, + isRequestingSettings: PropTypes.bool.isRequired, + isSavingSettings: PropTypes.bool.isRequired, + isSimple: PropTypes.bool.isRequired, + isVip: PropTypes.bool.isRequired, + onChangeField: PropTypes.func.isRequired, + setFieldValue: PropTypes.func.isRequired, + settings: PropTypes.object.isRequired, + }; + + static defaultProps = { + isSavingSettings: false, + isRequestingSettings: true, + fields: {}, + settings: {}, + }; + + disableForm() { + return ( + this.props.moduleDetailsLoading || + this.props.isRequestingSettings || + this.props.isSavingSettings + ); + } + + wafModuleSupported = () => { + return ( + this.props.firewallModuleAvailable && + ! this.props.isAtomic && + ! this.props.isVip && + ! this.props.isSimple + ); + }; + + disableWafForm = () => { + return this.disableForm() || ! this.wafModuleSupported(); + }; + + ensureWafModuleActive = () => { + if ( this.wafModuleSupported() && ! this.props.firewallModuleActive ) { + this.props.activateModule( this.props.selectedSiteId, 'waf', true ); + } + }; + + hasAutomaticRulesInstalled = () => { + return !! this.props.settings.jetpack_waf_automatic_rules_last_updated_timestamp; + }; + + canUpdateAutomaticRules = () => { + return this.props.hasRequiredFeature || this.hasAutomaticRulesInstalled(); + }; + + automaticRulesLastUpdated = () => { + const timestamp = parseInt( + this.props.settings.jetpack_waf_automatic_rules_last_updated_timestamp + ); + if ( timestamp ) { + return moment( timestamp * 1000 ).format( 'MMMM D, YYYY h:mm A' ); + } + return ''; + }; + + handleAutomaticRulesToggle = ( event ) => { + this.ensureWafModuleActive(); + this.props.handleAutosavingToggle( 'jetpack_waf_automatic_rules' )( event ); + }; + + handleBlockListToggle = ( event ) => { + this.ensureWafModuleActive(); + this.props.handleAutosavingToggle( 'jetpack_waf_ip_block_list_enabled' )( event ); + }; + + handleSubmitForm = ( event ) => { + this.ensureWafModuleActive(); + return this.props.handleSubmitForm( event ); + }; + + render() { + const { + dirtyFields, + disableProtect, + fields, + handleAutosavingToggle, + hasRequiredFeature, + isRequestingSettings, + isSavingSettings, + onChangeField, + selectedSiteId, + selectedSiteSlug, + setFieldValue, + settings, + translate, + } = this.props; + + return ( +
+ + + + + { /* Automatic Rules */ } + { this.wafModuleSupported() && this.canUpdateAutomaticRules() && ( + + + + { this.automaticRulesLastUpdated() && ( + + { translate( 'Automatic security rules installed. Last updated on %(date)s.', { + args: { + date: this.automaticRulesLastUpdated(), + }, + } ) } + + ) } + + { translate( + 'Block untrusted traffic sources by scanning every request made to your site. Jetpackā€™s advanced security rules are automatically kept up-to-date to protect your site from the latest threats.' + ) } + + + + ) } + + { /* Upgrade Prompt */ } + { ! hasRequiredFeature && this.wafModuleSupported() && ( + + ) } + + { /* Brute Force Login Protection */ } + + + { /* IP Block List */ } + { this.wafModuleSupported() && settings.jetpack_waf_ip_block_list_enabled !== undefined && ( + + + + + { translate( + 'IP addresses added to this list will be blocked from accessing your site.' + ) } + + { fields.jetpack_waf_ip_block_list_enabled && ( +
+ +
+ ) } +
+
+ ) } + + { /* IP Allow List */ } + +
+ ); + } +} + +export default connect( + ( state ) => { + const selectedSiteSlug = getSelectedSiteSlug( state ); + const selectedSiteId = getSelectedSiteId( state ); + const moduleDetailsLoading = isFetchingJetpackModules( state, selectedSiteId ); + const firewallModuleDetails = getJetpackModule( state, selectedSiteId, 'waf' ); + const siteInDevMode = isJetpackSiteInDevelopmentMode( state, selectedSiteId ); + const moduleUnavailableInDevMode = isJetpackModuleUnavailableInDevelopmentMode( + state, + selectedSiteId, + 'waf' + ); + const hasRequiredFeature = siteHasFeature( state, selectedSiteId, WPCOM_FEATURES_SCAN ); + + return { + selectedSiteId, + selectedSiteSlug, + firewallModuleActive: !! isJetpackModuleActive( state, selectedSiteId, 'waf' ), + firewallModuleAvailable: + firewallModuleDetails !== null && ( ! siteInDevMode || ! moduleUnavailableInDevMode ), + moduleDetailsLoading, + hasRequiredFeature, + }; + }, + { + activateModule, + } +)( localize( Firewall ) ); diff --git a/client/my-sites/site-settings/form-security.jsx b/client/my-sites/site-settings/form-security.jsx index b2cfe83710925..ee056cdba7570 100644 --- a/client/my-sites/site-settings/form-security.jsx +++ b/client/my-sites/site-settings/form-security.jsx @@ -9,8 +9,10 @@ import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-act import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode'; import isJetpackSiteInDevelopmentMode from 'calypso/state/selectors/is-jetpack-site-in-development-mode'; import isSiteAutomatedTransfer from 'calypso/state/selectors/is-site-automated-transfer'; +import isVipSite from 'calypso/state/selectors/is-vip-site'; +import { isSimpleSite } from 'calypso/state/sites/selectors'; import { getSelectedSiteId } from 'calypso/state/ui/selectors'; -import Protect from './protect'; +import Firewall from './firewall'; import SpamFilteringSettings from './spam-filtering-settings'; import Sso from './sso'; import wrapSettingsForm from './wrap-settings-form'; @@ -24,6 +26,8 @@ class SiteSettingsFormSecurity extends Component { handleAutosavingToggle, handleSubmitForm, isAtomic, + isSimple, + isVip, isRequestingSettings, isSavingSettings, onChangeField, @@ -33,6 +37,7 @@ class SiteSettingsFormSecurity extends Component { settings, siteId, translate, + activateModule, } = this.props; const disableProtect = ! protectModuleActive || protectModuleUnavailable; const disableSpamFiltering = ! fields.akismet || akismetUnavailable; @@ -44,24 +49,25 @@ class SiteSettingsFormSecurity extends Component { className="site-settings__security-settings" > + - - - - { ! isAtomic && (
{ const siteId = getSelectedSiteId( state ); const isAtomic = isSiteAutomatedTransfer( state, siteId ); + const isSimple = isSimpleSite( state, siteId ); + const isVip = isVipSite( state, siteId ); const protectModuleActive = !! isJetpackModuleActive( state, siteId, 'protect' ); const siteInDevMode = isJetpackSiteInDevelopmentMode( state, siteId ); const protectIsUnavailableInDevMode = isJetpackModuleUnavailableInDevelopmentMode( @@ -114,6 +122,8 @@ const connectComponent = connect( ( state ) => { return { siteId, isAtomic, + isSimple, + isVip, protectModuleActive, protectModuleUnavailable: siteInDevMode && protectIsUnavailableInDevMode, akismetUnavailable: siteInDevMode && akismetIsUnavailableInDevMode, @@ -125,6 +135,12 @@ const getFormSettings = ( settings ) => 'akismet', 'protect', 'jetpack_protect_global_whitelist', + 'jetpack_waf_automatic_rules', + 'jetpack_waf_ip_allow_list', + 'jetpack_waf_ip_allow_list_enabled', + 'jetpack_waf_ip_block_list', + 'jetpack_waf_ip_block_list_enabled', + 'jetpack_waf_automatic_rules_last_updated_timestamp', 'sso', 'jetpack_sso_match_by_email', 'jetpack_sso_require_two_step', diff --git a/client/my-sites/site-settings/protect.jsx b/client/my-sites/site-settings/protect.jsx index 22a8158504f7d..f100d786ca1e7 100644 --- a/client/my-sites/site-settings/protect.jsx +++ b/client/my-sites/site-settings/protect.jsx @@ -1,14 +1,10 @@ -import { Button, FoldableCard, FormLabel } from '@automattic/components'; +import { Card } from '@automattic/components'; import { localize } from 'i18n-calypso'; -import { includes, some } from 'lodash'; import PropTypes from 'prop-types'; import { Component } from 'react'; import { connect } from 'react-redux'; -import QueryJetpackConnection from 'calypso/components/data/query-jetpack-connection'; import FormFieldset from 'calypso/components/forms/form-fieldset'; import FormSettingExplanation from 'calypso/components/forms/form-setting-explanation'; -import FormTextarea from 'calypso/components/forms/form-textarea'; -import SupportInfo from 'calypso/components/support-info'; import JetpackModuleToggle from 'calypso/my-sites/site-settings/jetpack-module-toggle'; import isJetpackModuleActive from 'calypso/state/selectors/is-jetpack-module-active'; import isJetpackModuleUnavailableInDevelopmentMode from 'calypso/state/selectors/is-jetpack-module-unavailable-in-development-mode'; @@ -22,6 +18,7 @@ class Protect extends Component { isSavingSettings: PropTypes.bool, isRequestingSettings: PropTypes.bool, fields: PropTypes.object, + disableProtect: PropTypes.bool, }; static defaultProps = { @@ -30,136 +27,31 @@ class Protect extends Component { fields: {}, }; - handleAddToAllowedList = () => { - const { setFieldValue } = this.props; - let allowedIps = this.getProtectAllowedIps().trimEnd(); - - if ( allowedIps.length ) { - allowedIps += '\n'; - } - - setFieldValue( 'jetpack_protect_global_whitelist', allowedIps + this.getIpAddress() ); - }; - - getIpAddress() { - if ( window.app && window.app.clientIp ) { - return window.app.clientIp; - } - - return null; - } - - getProtectAllowedIps() { - const { jetpack_protect_global_whitelist } = this.props.fields; - return jetpack_protect_global_whitelist || ''; - } - - isIpAddressAllowed() { - const ipAddress = this.getIpAddress(); - if ( ! ipAddress ) { - return false; - } - - const allowedIps = this.getProtectAllowedIps().split( '\n' ); - - return ( - includes( allowedIps, ipAddress ) || - some( allowedIps, ( entry ) => { - if ( entry.indexOf( '-' ) < 0 ) { - return false; - } - - const range = entry.split( '-' ).map( ( ip ) => ip.trim() ); - return includes( range, ipAddress ); - } ) - ); - } - render() { const { isRequestingSettings, isSavingSettings, - onChangeField, - protectModuleActive, protectModuleUnavailable, selectedSiteId, translate, } = this.props; - const ipAddress = this.getIpAddress(); - const isIpAllowed = this.isIpAddressAllowed(); - const disabled = - isRequestingSettings || isSavingSettings || protectModuleUnavailable || ! protectModuleActive; - const protectToggle = ( - - ); - return ( - - - + -
- -

- { translate( 'Your current IP address: {{strong}}%(IP)s{{/strong}}{{br/}}', { - args: { - IP: ipAddress || translate( 'Unknown IP address' ), - }, - components: { - strong: , - br:
, - }, - } ) } - - { ipAddress && ( - - ) } -

- - - { translate( 'Allowed IP addresses' ) } - - - - { translate( - 'You may explicitly allow an IP address or series of addresses preventing them from ' + - 'ever being blocked by Jetpack. IPv4 and IPv6 are acceptable. ' + - 'To specify a range, enter the low value and high value separated by a dash. ' + - 'Example: 12.12.12.1-12.12.12.100' - ) } - -
+ + + { translate( + 'Prevent and block unwanted login attempts from bots and hackers attempting to log in to your website with common username and password combinations.' + ) } +
-
+ ); } } diff --git a/client/my-sites/site-settings/style.scss b/client/my-sites/site-settings/style.scss index c86425b8b5814..00527cb02e655 100644 --- a/client/my-sites/site-settings/style.scss +++ b/client/my-sites/site-settings/style.scss @@ -753,3 +753,12 @@ button.site-settings__general-settings-set-guessed-timezone.button.is-borderless .verbum-comments-toggle { flex-direction: column; } + +.site-settings__firewall-settings .card:not(:last-child) { + margin-bottom: 0; +} + +.site-settings__security-settings-firewall-nudge { + margin-bottom: 0; + z-index: 1; +}