Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com> (cherry picked from commit 8911386ed0fcaa5ed0a894e511a81ecc87e58d49)pull/4220/head
parent
77a60141bd
commit
cdd683ae8f
@ -0,0 +1,34 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AuthenticationRequiredModalContentConnector from './AuthenticationRequiredModalContentConnector';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModal(props) {
|
||||
const {
|
||||
isOpen
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size={sizes.MEDIUM}
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<AuthenticationRequiredModalContentConnector
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModal;
|
@ -0,0 +1,5 @@
|
||||
.authRequiredAlert {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
margin-bottom: 20px;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'authRequiredAlert': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,164 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import FormGroup from 'Components/Form/FormGroup';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import FormLabel from 'Components/Form/FormLabel';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings';
|
||||
import styles from './AuthenticationRequiredModalContent.css';
|
||||
|
||||
function onModalClose() {
|
||||
// No-op
|
||||
}
|
||||
|
||||
function AuthenticationRequiredModalContent(props) {
|
||||
const {
|
||||
isPopulated,
|
||||
error,
|
||||
isSaving,
|
||||
settings,
|
||||
onInputChange,
|
||||
onSavePress,
|
||||
dispatchFetchStatus
|
||||
} = props;
|
||||
|
||||
const {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
|
||||
const didMount = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isSaving && didMount.current) {
|
||||
dispatchFetchStatus();
|
||||
}
|
||||
|
||||
didMount.current = true;
|
||||
}, [isSaving, dispatchFetchStatus]);
|
||||
|
||||
return (
|
||||
<ModalContent
|
||||
showCloseButton={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ModalHeader>
|
||||
Authentication Required
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<Alert
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{authenticationRequiredWarning}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>Authentication</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText="Require Username and Password to access Radarr"
|
||||
onChange={onInputChange}
|
||||
{...authenticationMethod}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>Authentication Required</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationRequired"
|
||||
values={authenticationRequiredOptions}
|
||||
helpText="Change which requests authentication is required for. Do not change unless you understand the risks."
|
||||
onChange={onInputChange}
|
||||
{...authenticationRequired}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>Username</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.TEXT}
|
||||
name="username"
|
||||
onChange={onInputChange}
|
||||
{...username}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
authenticationEnabled ?
|
||||
<FormGroup>
|
||||
<FormLabel>Password</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="password"
|
||||
onChange={onInputChange}
|
||||
{...password}
|
||||
/>
|
||||
</FormGroup> :
|
||||
null
|
||||
}
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
!isPopulated && !error ? <LoadingIndicator /> : null
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<SpinnerButton
|
||||
kind={kinds.PRIMARY}
|
||||
isSpinning={isSaving}
|
||||
isDisabled={!authenticationEnabled}
|
||||
onPress={onSavePress}
|
||||
>
|
||||
Save
|
||||
</SpinnerButton>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContent.propTypes = {
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
isSaving: PropTypes.bool.isRequired,
|
||||
saveError: PropTypes.object,
|
||||
settings: PropTypes.object.isRequired,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
onSavePress: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default AuthenticationRequiredModalContent;
|
@ -0,0 +1,86 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { clearPendingChanges } from 'Store/Actions/baseActions';
|
||||
import { fetchGeneralSettings, saveGeneralSettings, setGeneralSettingsValue } from 'Store/Actions/settingsActions';
|
||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||
import createSettingsSectionSelector from 'Store/Selectors/createSettingsSectionSelector';
|
||||
import AuthenticationRequiredModalContent from './AuthenticationRequiredModalContent';
|
||||
|
||||
const SECTION = 'general';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSettingsSectionSelector(SECTION),
|
||||
(sectionSettings) => {
|
||||
return {
|
||||
...sectionSettings
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchClearPendingChanges: clearPendingChanges,
|
||||
dispatchSetGeneralSettingsValue: setGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings: saveGeneralSettings,
|
||||
dispatchFetchGeneralSettings: fetchGeneralSettings,
|
||||
dispatchFetchStatus: fetchStatus
|
||||
};
|
||||
|
||||
class AuthenticationRequiredModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dispatchFetchGeneralSettings();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.dispatchClearPendingChanges({ section: `settings.${SECTION}` });
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onInputChange = ({ name, value }) => {
|
||||
this.props.dispatchSetGeneralSettingsValue({ name, value });
|
||||
};
|
||||
|
||||
onSavePress = () => {
|
||||
this.props.dispatchSaveGeneralSettings();
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
dispatchClearPendingChanges,
|
||||
dispatchFetchGeneralSettings,
|
||||
dispatchSetGeneralSettingsValue,
|
||||
dispatchSaveGeneralSettings,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<AuthenticationRequiredModalContent
|
||||
{...otherProps}
|
||||
onInputChange={this.onInputChange}
|
||||
onSavePress={this.onSavePress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
AuthenticationRequiredModalContentConnector.propTypes = {
|
||||
dispatchClearPendingChanges: PropTypes.func.isRequired,
|
||||
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchSetGeneralSettingsValue: PropTypes.func.isRequired,
|
||||
dispatchSaveGeneralSettings: PropTypes.func.isRequired,
|
||||
dispatchFetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(AuthenticationRequiredModalContentConnector);
|
@ -0,0 +1,8 @@
|
||||
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||
|
||||
namespace NzbDrone.Http.Authentication
|
||||
{
|
||||
public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
using System.Net;
|
||||
using System.Threading.Tasks;
|
||||
using Lidarr.Http.Extensions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Authentication;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Configuration.Events;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Http.Authentication
|
||||
{
|
||||
public class UiAuthorizationHandler : AuthorizationHandler<BypassableDenyAnonymousAuthorizationRequirement>, IAuthorizationRequirement, IHandle<ConfigSavedEvent>
|
||||
{
|
||||
private readonly IConfigFileProvider _configService;
|
||||
private static AuthenticationRequiredType _authenticationRequired;
|
||||
|
||||
public UiAuthorizationHandler(IConfigFileProvider configService)
|
||||
{
|
||||
_configService = configService;
|
||||
_authenticationRequired = configService.AuthenticationRequired;
|
||||
}
|
||||
|
||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BypassableDenyAnonymousAuthorizationRequirement requirement)
|
||||
{
|
||||
if (_authenticationRequired == AuthenticationRequiredType.DisabledForLocalAddresses)
|
||||
{
|
||||
if (context.Resource is HttpContext httpContext &&
|
||||
IPAddress.TryParse(httpContext.GetRemoteIP(), out var ipAddress) &&
|
||||
ipAddress.IsLocalAddress())
|
||||
{
|
||||
context.Succeed(requirement);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void Handle(ConfigSavedEvent message)
|
||||
{
|
||||
_authenticationRequired = _configService.AuthenticationRequired;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
namespace NzbDrone.Core.Authentication
|
||||
{
|
||||
public enum AuthenticationRequiredType
|
||||
{
|
||||
Enabled = 0,
|
||||
DisabledForLocalAddresses = 1
|
||||
}
|
||||
}
|
Loading…
Reference in new issue