mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Auth Required
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import AppUpdatedModalConnector from 'App/AppUpdatedModalConnector';
|
|||||||
import ColorImpairedContext from 'App/ColorImpairedContext';
|
import ColorImpairedContext from 'App/ColorImpairedContext';
|
||||||
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
import ConnectionLostModalConnector from 'App/ConnectionLostModalConnector';
|
||||||
import SignalRConnector from 'Components/SignalRConnector';
|
import SignalRConnector from 'Components/SignalRConnector';
|
||||||
|
import AuthenticationRequiredModal from 'FirstRun/AuthenticationRequiredModal';
|
||||||
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
import locationShape from 'Helpers/Props/Shapes/locationShape';
|
||||||
import PageHeader from './Header/PageHeader';
|
import PageHeader from './Header/PageHeader';
|
||||||
import PageSidebar from './Sidebar/PageSidebar';
|
import PageSidebar from './Sidebar/PageSidebar';
|
||||||
@@ -75,6 +76,7 @@ class Page extends Component {
|
|||||||
isSmallScreen,
|
isSmallScreen,
|
||||||
isSidebarVisible,
|
isSidebarVisible,
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
|
authenticationEnabled,
|
||||||
onSidebarToggle,
|
onSidebarToggle,
|
||||||
onSidebarVisibleChange
|
onSidebarVisibleChange
|
||||||
} = this.props;
|
} = this.props;
|
||||||
@@ -109,6 +111,10 @@ class Page extends Component {
|
|||||||
isOpen={this.state.isConnectionLostModalOpen}
|
isOpen={this.state.isConnectionLostModalOpen}
|
||||||
onModalClose={this.onConnectionLostModalClose}
|
onModalClose={this.onConnectionLostModalClose}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<AuthenticationRequiredModal
|
||||||
|
isOpen={!authenticationEnabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ColorImpairedContext.Provider>
|
</ColorImpairedContext.Provider>
|
||||||
);
|
);
|
||||||
@@ -124,6 +130,7 @@ Page.propTypes = {
|
|||||||
isUpdated: PropTypes.bool.isRequired,
|
isUpdated: PropTypes.bool.isRequired,
|
||||||
isDisconnected: PropTypes.bool.isRequired,
|
isDisconnected: PropTypes.bool.isRequired,
|
||||||
enableColorImpairedMode: PropTypes.bool.isRequired,
|
enableColorImpairedMode: PropTypes.bool.isRequired,
|
||||||
|
authenticationEnabled: PropTypes.bool.isRequired,
|
||||||
onResize: PropTypes.func.isRequired,
|
onResize: PropTypes.func.isRequired,
|
||||||
onSidebarToggle: PropTypes.func.isRequired,
|
onSidebarToggle: PropTypes.func.isRequired,
|
||||||
onSidebarVisibleChange: PropTypes.func.isRequired
|
onSidebarVisibleChange: PropTypes.func.isRequired
|
||||||
|
@@ -11,6 +11,7 @@ import { fetchAppProfiles, fetchGeneralSettings, fetchIndexerCategories, fetchUI
|
|||||||
import { fetchStatus } from 'Store/Actions/systemActions';
|
import { fetchStatus } from 'Store/Actions/systemActions';
|
||||||
import { fetchTags } from 'Store/Actions/tagActions';
|
import { fetchTags } from 'Store/Actions/tagActions';
|
||||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||||
|
import createSystemStatusSelector from 'Store/Selectors/createSystemStatusSelector';
|
||||||
import ErrorPage from './ErrorPage';
|
import ErrorPage from './ErrorPage';
|
||||||
import LoadingPage from './LoadingPage';
|
import LoadingPage from './LoadingPage';
|
||||||
import Page from './Page';
|
import Page from './Page';
|
||||||
@@ -133,18 +134,21 @@ function createMapStateToProps() {
|
|||||||
selectErrors,
|
selectErrors,
|
||||||
selectAppProps,
|
selectAppProps,
|
||||||
createDimensionsSelector(),
|
createDimensionsSelector(),
|
||||||
|
createSystemStatusSelector(),
|
||||||
(
|
(
|
||||||
enableColorImpairedMode,
|
enableColorImpairedMode,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
errors,
|
errors,
|
||||||
app,
|
app,
|
||||||
dimensions
|
dimensions,
|
||||||
|
systemStatus
|
||||||
) => {
|
) => {
|
||||||
return {
|
return {
|
||||||
...app,
|
...app,
|
||||||
...errors,
|
...errors,
|
||||||
isPopulated,
|
isPopulated,
|
||||||
isSmallScreen: dimensions.isSmallScreen,
|
isSmallScreen: dimensions.isSmallScreen,
|
||||||
|
authenticationEnabled: systemStatus.authentication !== 'none',
|
||||||
enableColorImpairedMode
|
enableColorImpairedMode
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
34
frontend/src/FirstRun/AuthenticationRequiredModal.js
Normal file
@@ -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;
|
||||||
|
}
|
164
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
164
frontend/src/FirstRun/AuthenticationRequiredModalContent.js
Normal file
@@ -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 Sonarr"
|
||||||
|
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);
|
@@ -11,12 +11,20 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
|
|||||||
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
import { icons, inputTypes, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
|
||||||
const authenticationMethodOptions = [
|
export const authenticationRequiredWarning = 'To prevent remote access without authentication, Sonarr now requires authentication to be enabled. You can optionally disable authentication from local addresses.';
|
||||||
{ key: 'none', value: 'None' },
|
|
||||||
|
export const authenticationMethodOptions = [
|
||||||
|
{ key: 'none', value: 'None', isDisabled: true },
|
||||||
|
{ key: 'external', value: 'External', isHidden: true },
|
||||||
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
{ key: 'basic', value: 'Basic (Browser Popup)' },
|
||||||
{ key: 'forms', value: 'Forms (Login Page)' }
|
{ key: 'forms', value: 'Forms (Login Page)' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const authenticationRequiredOptions = [
|
||||||
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
|
||||||
|
];
|
||||||
|
|
||||||
const certificateValidationOptions = [
|
const certificateValidationOptions = [
|
||||||
{ key: 'enabled', value: 'Enabled' },
|
{ key: 'enabled', value: 'Enabled' },
|
||||||
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
|
||||||
@@ -68,6 +76,7 @@ class SecuritySettings extends Component {
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
authenticationMethod,
|
authenticationMethod,
|
||||||
|
authenticationRequired,
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
apiKey,
|
apiKey,
|
||||||
@@ -86,13 +95,31 @@ class SecuritySettings extends Component {
|
|||||||
name="authenticationMethod"
|
name="authenticationMethod"
|
||||||
values={authenticationMethodOptions}
|
values={authenticationMethodOptions}
|
||||||
helpText={translate('AuthenticationMethodHelpText')}
|
helpText={translate('AuthenticationMethodHelpText')}
|
||||||
|
helpTextWarning={authenticationRequiredWarning}
|
||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...authenticationMethod}
|
{...authenticationMethod}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
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>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Username')}</FormLabel>
|
<FormLabel>{translate('Username')}</FormLabel>
|
||||||
|
|
||||||
@@ -102,11 +129,12 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...username}
|
{...username}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
authenticationEnabled &&
|
authenticationEnabled ?
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
<FormLabel>{translate('Password')}</FormLabel>
|
<FormLabel>{translate('Password')}</FormLabel>
|
||||||
|
|
||||||
@@ -116,7 +144,8 @@ class SecuritySettings extends Component {
|
|||||||
onChange={onInputChange}
|
onChange={onInputChange}
|
||||||
{...password}
|
{...password}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup> :
|
||||||
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
<FormGroup>
|
<FormGroup>
|
||||||
|
@@ -46,7 +46,7 @@ namespace NzbDrone.Automation.Test
|
|||||||
|
|
||||||
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
_runner = new NzbDroneRunner(LogManager.GetCurrentClassLogger(), null);
|
||||||
_runner.KillAll();
|
_runner.KillAll();
|
||||||
_runner.Start();
|
_runner.Start(true);
|
||||||
|
|
||||||
driver.Url = "http://localhost:9696";
|
driver.Url = "http://localhost:9696";
|
||||||
|
|
||||||
|
@@ -11,29 +11,44 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
sentryEvent.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
if (sentryEvent.Message is not null)
|
||||||
|
{
|
||||||
|
sentryEvent.Message.Formatted = CleanseLogMessage.Cleanse(sentryEvent.Message.Formatted);
|
||||||
|
sentryEvent.Message.Message = CleanseLogMessage.Cleanse(sentryEvent.Message.Message);
|
||||||
|
sentryEvent.Message.Params = sentryEvent.Message.Params?.Select(x => CleanseLogMessage.Cleanse(x switch
|
||||||
|
{
|
||||||
|
string str => str,
|
||||||
|
_ => x.ToString()
|
||||||
|
})).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
if (sentryEvent.Fingerprint != null)
|
if (sentryEvent.Fingerprint.Any())
|
||||||
{
|
{
|
||||||
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
var fingerprint = sentryEvent.Fingerprint.Select(x => CleanseLogMessage.Cleanse(x)).ToList();
|
||||||
sentryEvent.SetFingerprint(fingerprint);
|
sentryEvent.SetFingerprint(fingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sentryEvent.Extra != null)
|
if (sentryEvent.Extra.Any())
|
||||||
{
|
{
|
||||||
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse((string)y.Value));
|
var extras = sentryEvent.Extra.ToDictionary(x => x.Key, y => (object)CleanseLogMessage.Cleanse(y.Value as string));
|
||||||
sentryEvent.SetExtras(extras);
|
sentryEvent.SetExtras(extras);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sentryEvent.SentryExceptions is not null)
|
||||||
|
{
|
||||||
foreach (var exception in sentryEvent.SentryExceptions)
|
foreach (var exception in sentryEvent.SentryExceptions)
|
||||||
{
|
{
|
||||||
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
exception.Value = CleanseLogMessage.Cleanse(exception.Value);
|
||||||
|
if (exception.Stacktrace is not null)
|
||||||
|
{
|
||||||
foreach (var frame in exception.Stacktrace.Frames)
|
foreach (var frame in exception.Stacktrace.Frames)
|
||||||
{
|
{
|
||||||
frame.FileName = ShortenPath(frame.FileName);
|
frame.FileName = ShortenPath(frame.FileName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (Exception)
|
catch (Exception)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@@ -42,10 +42,7 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
"UnauthorizedAccessException",
|
"UnauthorizedAccessException",
|
||||||
|
|
||||||
// Filter out people stuck in boot loops
|
// Filter out people stuck in boot loops
|
||||||
"CorruptDatabaseException",
|
"CorruptDatabaseException"
|
||||||
|
|
||||||
// This also filters some people in boot loops
|
|
||||||
"TinyIoCResolutionException"
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
public static readonly List<string> FilteredExceptionMessages = new List<string>
|
||||||
@@ -102,9 +99,6 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
o.Dsn = dsn;
|
o.Dsn = dsn;
|
||||||
o.AttachStacktrace = true;
|
o.AttachStacktrace = true;
|
||||||
o.MaxBreadcrumbs = 200;
|
o.MaxBreadcrumbs = 200;
|
||||||
o.SendDefaultPii = false;
|
|
||||||
o.Debug = false;
|
|
||||||
o.DiagnosticLevel = SentryLevel.Debug;
|
|
||||||
o.Release = BuildInfo.Release;
|
o.Release = BuildInfo.Release;
|
||||||
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
o.BeforeSend = x => SentryCleanser.CleanseEvent(x);
|
||||||
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
o.BeforeBreadcrumb = x => SentryCleanser.CleanseBreadcrumb(x);
|
||||||
@@ -210,7 +204,11 @@ namespace NzbDrone.Common.Instrumentation.Sentry
|
|||||||
if (ex != null)
|
if (ex != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.GetType().FullName);
|
fingerPrint.Add(ex.GetType().FullName);
|
||||||
|
if (ex.TargetSite != null)
|
||||||
|
{
|
||||||
fingerPrint.Add(ex.TargetSite.ToString());
|
fingerPrint.Add(ex.TargetSite.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
if (ex.InnerException != null)
|
if (ex.InnerException != null)
|
||||||
{
|
{
|
||||||
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
fingerPrint.Add(ex.InnerException.GetType().FullName);
|
||||||
|
@@ -4,13 +4,13 @@
|
|||||||
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
<DefineConstants Condition="'$(RuntimeIdentifier)' == 'linux-musl-x64' or '$(RuntimeIdentifier)' == 'linux-musl-arm64'">ISMUSL</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="6.0.1" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||||
<PackageReference Include="Sentry" Version="3.21.0" />
|
<PackageReference Include="Sentry" Version="3.24.1" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
<PackageReference Include="SharpZipLib" Version="1.3.3" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="NBuilder" Version="6.1.0" />
|
<PackageReference Include="NBuilder" Version="6.1.0" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Test.Common\Prowlarr.Test.Common.csproj" />
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
namespace NzbDrone.Core.Authentication
|
||||||
|
{
|
||||||
|
public enum AuthenticationRequiredType
|
||||||
|
{
|
||||||
|
Enabled = 0,
|
||||||
|
DisabledForLocalAddresses = 1
|
||||||
|
}
|
||||||
|
}
|
@@ -1,9 +1,10 @@
|
|||||||
namespace NzbDrone.Core.Authentication
|
namespace NzbDrone.Core.Authentication
|
||||||
{
|
{
|
||||||
public enum AuthenticationType
|
public enum AuthenticationType
|
||||||
{
|
{
|
||||||
None = 0,
|
None = 0,
|
||||||
Basic = 1,
|
Basic = 1,
|
||||||
Forms = 2
|
Forms = 2,
|
||||||
|
External = 3
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using NzbDrone.Core.Datastore;
|
using NzbDrone.Core.Datastore;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Authentication
|
namespace NzbDrone.Core.Authentication
|
||||||
@@ -8,5 +8,7 @@ namespace NzbDrone.Core.Authentication
|
|||||||
public Guid Identifier { get; set; }
|
public Guid Identifier { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
|
public string Salt { get; set; }
|
||||||
|
public int Iterations { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
||||||
using NzbDrone.Common.Disk;
|
using NzbDrone.Common.Disk;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Common.Extensions;
|
using NzbDrone.Common.Extensions;
|
||||||
@@ -21,6 +23,10 @@ namespace NzbDrone.Core.Authentication
|
|||||||
private readonly IAppFolderInfo _appFolderInfo;
|
private readonly IAppFolderInfo _appFolderInfo;
|
||||||
private readonly IDiskProvider _diskProvider;
|
private readonly IDiskProvider _diskProvider;
|
||||||
|
|
||||||
|
private static readonly int ITERATIONS = 10000;
|
||||||
|
private static readonly int SALT_SIZE = 128 / 8;
|
||||||
|
private static readonly int NUMBER_OF_BYTES = 256 / 8;
|
||||||
|
|
||||||
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
public UserService(IUserRepository repo, IAppFolderInfo appFolderInfo, IDiskProvider diskProvider)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
@@ -30,12 +36,15 @@ namespace NzbDrone.Core.Authentication
|
|||||||
|
|
||||||
public User Add(string username, string password)
|
public User Add(string username, string password)
|
||||||
{
|
{
|
||||||
return _repo.Insert(new User
|
var user = new User
|
||||||
{
|
{
|
||||||
Identifier = Guid.NewGuid(),
|
Identifier = Guid.NewGuid(),
|
||||||
Username = username.ToLowerInvariant(),
|
Username = username.ToLowerInvariant()
|
||||||
Password = password.SHA256Hash()
|
};
|
||||||
});
|
|
||||||
|
SetUserHashedPassword(user, password);
|
||||||
|
|
||||||
|
return _repo.Insert(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User Update(User user)
|
public User Update(User user)
|
||||||
@@ -54,7 +63,7 @@ namespace NzbDrone.Core.Authentication
|
|||||||
|
|
||||||
if (user.Password != password)
|
if (user.Password != password)
|
||||||
{
|
{
|
||||||
user.Password = password.SHA256Hash();
|
SetUserHashedPassword(user, password);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Username = username.ToLowerInvariant();
|
user.Username = username.ToLowerInvariant();
|
||||||
@@ -81,7 +90,20 @@ namespace NzbDrone.Core.Authentication
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (user.Salt.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
// If password matches stored SHA256 hash, update to salted hash and verify.
|
||||||
if (user.Password == password.SHA256Hash())
|
if (user.Password == password.SHA256Hash())
|
||||||
|
{
|
||||||
|
SetUserHashedPassword(user, password);
|
||||||
|
|
||||||
|
return Update(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (VerifyHashedPassword(user, password))
|
||||||
{
|
{
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
@@ -93,5 +115,42 @@ namespace NzbDrone.Core.Authentication
|
|||||||
{
|
{
|
||||||
return _repo.FindUser(identifier);
|
return _repo.FindUser(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private User SetUserHashedPassword(User user, string password)
|
||||||
|
{
|
||||||
|
var salt = GenerateSalt();
|
||||||
|
|
||||||
|
user.Iterations = ITERATIONS;
|
||||||
|
user.Salt = Convert.ToBase64String(salt);
|
||||||
|
user.Password = GetHashedPassword(password, salt, ITERATIONS);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] GenerateSalt()
|
||||||
|
{
|
||||||
|
var salt = new byte[SALT_SIZE];
|
||||||
|
RandomNumberGenerator.Create().GetBytes(salt);
|
||||||
|
|
||||||
|
return salt;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetHashedPassword(string password, byte[] salt, int iterations)
|
||||||
|
{
|
||||||
|
return Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
||||||
|
password: password,
|
||||||
|
salt: salt,
|
||||||
|
prf: KeyDerivationPrf.HMACSHA512,
|
||||||
|
iterationCount: iterations,
|
||||||
|
numBytesRequested: NUMBER_OF_BYTES));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool VerifyHashedPassword(User user, string password)
|
||||||
|
{
|
||||||
|
var salt = Convert.FromBase64String(user.Salt);
|
||||||
|
var hashedPassword = GetHashedPassword(password, salt, user.Iterations);
|
||||||
|
|
||||||
|
return user.Password == hashedPassword;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -32,6 +32,7 @@ namespace NzbDrone.Core.Configuration
|
|||||||
bool EnableSsl { get; }
|
bool EnableSsl { get; }
|
||||||
bool LaunchBrowser { get; }
|
bool LaunchBrowser { get; }
|
||||||
AuthenticationType AuthenticationMethod { get; }
|
AuthenticationType AuthenticationMethod { get; }
|
||||||
|
AuthenticationRequiredType AuthenticationRequired { get; }
|
||||||
bool AnalyticsEnabled { get; }
|
bool AnalyticsEnabled { get; }
|
||||||
string LogLevel { get; }
|
string LogLevel { get; }
|
||||||
string ConsoleLogLevel { get; }
|
string ConsoleLogLevel { get; }
|
||||||
@@ -193,6 +194,8 @@ namespace NzbDrone.Core.Configuration
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AuthenticationRequiredType AuthenticationRequired => GetValueEnum("AuthenticationRequired", AuthenticationRequiredType.Enabled);
|
||||||
|
|
||||||
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
public bool AnalyticsEnabled => GetValueBoolean("AnalyticsEnabled", true, persist: false);
|
||||||
|
|
||||||
// TODO: Change back to "master" for the first stable release.
|
// TODO: Change back to "master" for the first stable release.
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(024)]
|
||||||
|
public class add_salt_to_users : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("Users")
|
||||||
|
.AddColumn("Salt").AsString().Nullable()
|
||||||
|
.AddColumn("Iterations").AsInt32().Nullable();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -6,7 +6,8 @@
|
|||||||
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
|
<PackageReference Include="AngleSharp.Xml" Version="0.17.0" />
|
||||||
<PackageReference Include="Dapper" Version="2.0.123" />
|
<PackageReference Include="Dapper" Version="2.0.123" />
|
||||||
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
|
<PackageReference Include="FluentMigrator.Runner" Version="3.3.2" />
|
||||||
<PackageReference Include="MailKit" Version="3.4.1" />
|
<PackageReference Include="MailKit" Version="3.4.3" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.12" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.WebUtilities" Version="2.2.0" />
|
||||||
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
<PackageReference Include="NLog.Targets.Syslog" Version="7.0.0" />
|
||||||
<PackageReference Include="Npgsql" Version="5.0.11" />
|
<PackageReference Include="Npgsql" Version="5.0.11" />
|
||||||
@@ -15,13 +16,13 @@
|
|||||||
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
|
<PackageReference Include="FluentMigrator.Runner.SQLite" Version="3.3.2" />
|
||||||
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
|
<PackageReference Include="FluentMigrator.Runner.Postgres" Version="3.3.2" />
|
||||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="TinyTwitter" Version="1.1.2" />
|
<PackageReference Include="TinyTwitter" Version="1.1.2" />
|
||||||
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
<PackageReference Include="System.Data.SQLite.Core.Servarr" Version="1.0.115.5-18" />
|
||||||
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
<PackageReference Include="System.Text.Json" Version="6.0.5" />
|
||||||
<PackageReference Include="MonoTorrent" Version="2.0.5" />
|
<PackageReference Include="MonoTorrent" Version="2.0.5" />
|
||||||
<PackageReference Include="YamlDotNet" Version="12.0.1" />
|
<PackageReference Include="YamlDotNet" Version="12.3.1" />
|
||||||
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
<PackageReference Include="AngleSharp" Version="0.17.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -4,11 +4,11 @@
|
|||||||
<OutputType>Library</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NLog.Extensions.Logging" Version="5.0.0" />
|
<PackageReference Include="NLog.Extensions.Logging" Version="5.2.0" />
|
||||||
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
<PackageReference Include="System.Text.Encoding.CodePages" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.1" />
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.4.0" />
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -22,7 +22,6 @@ using NzbDrone.Core.Instrumentation;
|
|||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
using NzbDrone.Core.Messaging.Events;
|
using NzbDrone.Core.Messaging.Events;
|
||||||
using NzbDrone.Host.AccessControl;
|
using NzbDrone.Host.AccessControl;
|
||||||
using NzbDrone.Http.Authentication;
|
|
||||||
using NzbDrone.SignalR;
|
using NzbDrone.SignalR;
|
||||||
using Prowlarr.Api.V1.System;
|
using Prowlarr.Api.V1.System;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
@@ -172,6 +171,8 @@ namespace NzbDrone.Host
|
|||||||
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
|
.PersistKeysToFileSystem(new DirectoryInfo(Configuration["dataProtectionFolder"]));
|
||||||
|
|
||||||
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
|
services.AddSingleton<IAuthorizationPolicyProvider, UiAuthorizationPolicyProvider>();
|
||||||
|
services.AddSingleton<IAuthorizationHandler, UiAuthorizationHandler>();
|
||||||
|
|
||||||
services.AddAuthorization(options =>
|
services.AddAuthorization(options =>
|
||||||
{
|
{
|
||||||
options.AddPolicy("SignalR", policy =>
|
options.AddPolicy("SignalR", policy =>
|
||||||
|
@@ -37,12 +37,12 @@ namespace NzbDrone.Test.Common
|
|||||||
Port = port;
|
Port = port;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start(bool enableAuth = false)
|
||||||
{
|
{
|
||||||
AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID());
|
AppData = Path.Combine(TestContext.CurrentContext.TestDirectory, "_intg_" + TestBase.GetUID());
|
||||||
Directory.CreateDirectory(AppData);
|
Directory.CreateDirectory(AppData);
|
||||||
|
|
||||||
GenerateConfigFile();
|
GenerateConfigFile(enableAuth);
|
||||||
|
|
||||||
string consoleExe;
|
string consoleExe;
|
||||||
if (OsInfo.IsWindows)
|
if (OsInfo.IsWindows)
|
||||||
@@ -167,7 +167,7 @@ namespace NzbDrone.Test.Common
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void GenerateConfigFile()
|
private void GenerateConfigFile(bool enableAuth)
|
||||||
{
|
{
|
||||||
var configFile = Path.Combine(AppData, "config.xml");
|
var configFile = Path.Combine(AppData, "config.xml");
|
||||||
|
|
||||||
@@ -180,6 +180,8 @@ namespace NzbDrone.Test.Common
|
|||||||
new XElement(nameof(ConfigFileProvider.ApiKey), apiKey),
|
new XElement(nameof(ConfigFileProvider.ApiKey), apiKey),
|
||||||
new XElement(nameof(ConfigFileProvider.LogLevel), "trace"),
|
new XElement(nameof(ConfigFileProvider.LogLevel), "trace"),
|
||||||
new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false),
|
new XElement(nameof(ConfigFileProvider.AnalyticsEnabled), false),
|
||||||
|
new XElement(nameof(ConfigFileProvider.AuthenticationMethod), enableAuth ? "Forms" : "None"),
|
||||||
|
new XElement(nameof(ConfigFileProvider.AuthenticationRequired), "DisabledForLocalAddresses"),
|
||||||
new XElement(nameof(ConfigFileProvider.Port), Port)));
|
new XElement(nameof(ConfigFileProvider.Port), Port)));
|
||||||
|
|
||||||
var data = xDoc.ToString();
|
var data = xDoc.ToString();
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
<PackageReference Include="FluentAssertions" Version="5.10.3" />
|
||||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||||
<PackageReference Include="Moq" Version="4.17.2" />
|
<PackageReference Include="Moq" Version="4.17.2" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
<PackageReference Include="RestSharp" Version="106.15.0" />
|
||||||
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
<PackageReference Include="RestSharp.Serializers.SystemTextJson" Version="106.15.0" />
|
||||||
|
@@ -4,9 +4,9 @@
|
|||||||
<TargetFrameworks>net6.0</TargetFrameworks>
|
<TargetFrameworks>net6.0</TargetFrameworks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.2.2" />
|
<PackageReference Include="DryIoc.dll" Version="5.3.1" />
|
||||||
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
|
<ProjectReference Include="..\NzbDrone.Common\Prowlarr.Common.csproj" />
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@@ -15,6 +15,7 @@ namespace Prowlarr.Api.V1.Config
|
|||||||
public bool EnableSsl { get; set; }
|
public bool EnableSsl { get; set; }
|
||||||
public bool LaunchBrowser { get; set; }
|
public bool LaunchBrowser { get; set; }
|
||||||
public AuthenticationType AuthenticationMethod { get; set; }
|
public AuthenticationType AuthenticationMethod { get; set; }
|
||||||
|
public AuthenticationRequiredType AuthenticationRequired { get; set; }
|
||||||
public bool AnalyticsEnabled { get; set; }
|
public bool AnalyticsEnabled { get; set; }
|
||||||
public string Username { get; set; }
|
public string Username { get; set; }
|
||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
@@ -57,6 +58,7 @@ namespace Prowlarr.Api.V1.Config
|
|||||||
EnableSsl = model.EnableSsl,
|
EnableSsl = model.EnableSsl,
|
||||||
LaunchBrowser = model.LaunchBrowser,
|
LaunchBrowser = model.LaunchBrowser,
|
||||||
AuthenticationMethod = model.AuthenticationMethod,
|
AuthenticationMethod = model.AuthenticationMethod,
|
||||||
|
AuthenticationRequired = model.AuthenticationRequired,
|
||||||
AnalyticsEnabled = model.AnalyticsEnabled,
|
AnalyticsEnabled = model.AnalyticsEnabled,
|
||||||
|
|
||||||
//Username
|
//Username
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />
|
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />
|
||||||
|
@@ -13,6 +13,7 @@ namespace Prowlarr.Http.Authentication
|
|||||||
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
|
public class ApiKeyAuthenticationOptions : AuthenticationSchemeOptions
|
||||||
{
|
{
|
||||||
public const string DefaultScheme = "API Key";
|
public const string DefaultScheme = "API Key";
|
||||||
|
|
||||||
public string Scheme => DefaultScheme;
|
public string Scheme => DefaultScheme;
|
||||||
public string AuthenticationType = DefaultScheme;
|
public string AuthenticationType = DefaultScheme;
|
||||||
|
|
||||||
|
@@ -22,10 +22,16 @@ namespace Prowlarr.Http.Authentication
|
|||||||
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
|
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static AuthenticationBuilder AddExternal(this AuthenticationBuilder authenticationBuilder, string name)
|
||||||
|
{
|
||||||
|
return authenticationBuilder.AddScheme<AuthenticationSchemeOptions, NoAuthenticationHandler>(name, options => { });
|
||||||
|
}
|
||||||
|
|
||||||
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
|
public static AuthenticationBuilder AddAppAuthentication(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
return services.AddAuthentication()
|
return services.AddAuthentication()
|
||||||
.AddNone(AuthenticationType.None.ToString())
|
.AddNone(AuthenticationType.None.ToString())
|
||||||
|
.AddExternal(AuthenticationType.External.ToString())
|
||||||
.AddBasic(AuthenticationType.Basic.ToString())
|
.AddBasic(AuthenticationType.Basic.ToString())
|
||||||
.AddCookie(AuthenticationType.Forms.ToString(), options =>
|
.AddCookie(AuthenticationType.Forms.ToString(), options =>
|
||||||
{
|
{
|
||||||
|
@@ -0,0 +1,8 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization.Infrastructure;
|
||||||
|
|
||||||
|
namespace Prowlarr.Http.Authentication
|
||||||
|
{
|
||||||
|
public class BypassableDenyAnonymousAuthorizationRequirement : DenyAnonymousAuthorizationRequirement
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
45
src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs
Normal file
45
src/Prowlarr.Http/Authentication/UiAuthorizationHandler.cs
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
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;
|
||||||
|
using Prowlarr.Http.Extensions;
|
||||||
|
|
||||||
|
namespace Prowlarr.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Authorization;
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
|
|
||||||
namespace NzbDrone.Http.Authentication
|
namespace Prowlarr.Http.Authentication
|
||||||
{
|
{
|
||||||
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
public class UiAuthorizationPolicyProvider : IAuthorizationPolicyProvider
|
||||||
{
|
{
|
||||||
@@ -29,7 +29,8 @@ namespace NzbDrone.Http.Authentication
|
|||||||
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
|
if (policyName.Equals(POLICY_NAME, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
|
var policy = new AuthorizationPolicyBuilder(_config.AuthenticationMethod.ToString())
|
||||||
.RequireAuthenticatedUser();
|
.AddRequirements(new BypassableDenyAnonymousAuthorizationRequirement());
|
||||||
|
|
||||||
return Task.FromResult(policy.Build());
|
return Task.FromResult(policy.Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
<PackageReference Include="FluentValidation" Version="8.6.2" />
|
||||||
<PackageReference Include="ImpromptuInterface" Version="7.0.1" />
|
<PackageReference Include="ImpromptuInterface" Version="7.0.1" />
|
||||||
<PackageReference Include="NLog" Version="5.0.1" />
|
<PackageReference Include="NLog" Version="5.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />
|
<ProjectReference Include="..\NzbDrone.Core\Prowlarr.Core.csproj" />
|
||||||
|
Reference in New Issue
Block a user