config: switch to authMethod config for auth method selection

This commit is contained in:
Jesse Chan
2020-10-26 20:21:00 +08:00
parent 7e56cd99ac
commit ccb410d2e5
15 changed files with 64 additions and 34 deletions

View File

@@ -22,7 +22,7 @@ const AppWrapper: React.FC<AppWrapperProps> = (props: AppWrapperProps) => {
overlay = <LoadingOverlay dependencies={UIStore.dependencies} />;
}
if (AuthStore.isAuthenticated && !ClientStatusStore.isConnected && !ConfigStore.disableAuth) {
if (AuthStore.isAuthenticated && !ClientStatusStore.isConnected && ConfigStore.authMethod !== 'none') {
overlay = (
<div className="application__loading-overlay">
<div className="application__entry-barrier">

View File

@@ -128,7 +128,7 @@ class SettingsModal extends Component<WrappedComponentProps, SettingsModalStates
id: 'settings.tabs.resources',
}),
},
...(!ConfigStore.disableAuth
...(ConfigStore.authMethod !== 'none'
? {
authentication: {
content: AuthTab,

View File

@@ -6,7 +6,7 @@ import Logout from '../icons/Logout';
import Tooltip from '../general/Tooltip';
const LogoutButton = () => {
if (ConfigStore.disableAuth) {
if (ConfigStore.authMethod === 'none') {
return null;
}

View File

@@ -1,18 +1,19 @@
import {makeAutoObservable} from 'mobx';
import type {AuthMethod} from '@shared/schema/Auth';
import type {AuthVerificationPreloadConfigs} from '@shared/schema/api/auth';
class ConfigStore {
baseURI = window.location.pathname.substr(0, window.location.pathname.lastIndexOf('/') + 1);
disableAuth = false;
authMethod: AuthMethod = 'default';
pollInterval = 2000;
constructor() {
makeAutoObservable(this);
}
handlePreloadConfigs({disableAuth, pollInterval}: AuthVerificationPreloadConfigs) {
this.disableAuth = disableAuth != null ? disableAuth : false;
handlePreloadConfigs({authMethod, pollInterval}: AuthVerificationPreloadConfigs) {
this.authMethod = authMethod || 'default';
this.pollInterval = pollInterval || 2000;
}
}

View File

@@ -30,40 +30,47 @@ const {argv} = require('yargs')
})
.option('secret', {
alias: 's',
hidden: true,
describe: 'A unique secret, a random one will be generated if not provided',
type: 'string',
})
.option('auth', {
describe: 'Access control and user management method',
choices: ['default', 'none'],
})
.option('noauth', {
alias: 'n',
hidden: true,
default: false,
describe: "Disable Flood's builtin access control system, needs rthost+rtport OR rtsocket.",
describe: "Disable Flood's builtin access control system, deprecated, use auth=none instead",
type: 'boolean',
})
.option('rthost', {
describe: "Depends on noauth: Host of rTorrent's SCGI interface",
describe: "Host of rTorrent's SCGI interface",
type: 'string',
})
.option('rtport', {
describe: "Depends on noauth: Port of rTorrent's SCGI interface",
describe: "Port of rTorrent's SCGI interface",
type: 'number',
})
.option('rtsocket', {
conflicts: ['rthost', 'rtport'],
describe: "Depends on noauth: Path to rTorrent's SCGI unix socket",
describe: "Path to rTorrent's SCGI unix socket",
type: 'string',
})
.option('qburl', {
describe: 'Depends on noauth: URL to qBittorrent Web API',
describe: 'URL to qBittorrent Web API',
type: 'string',
})
.option('qbuser', {
describe: 'Depends on noauth: Username of qBittorrent Web API',
describe: 'Username of qBittorrent Web API',
type: 'string',
})
.option('qbpass', {
describe: 'Depends on noauth: Password of qBittorrent Web API',
describe: 'Password of qBittorrent Web API',
type: 'string',
})
.group(['rthost', 'rtport', 'rtsocket', 'qburl', 'qbuser', 'qbpass'], 'When auth=none:')
.option('ssl', {
default: false,
describe: 'Enable SSL, key.pem and fullchain.pem needed in runtime directory',
@@ -195,12 +202,17 @@ if (argv.rtsocket != null || argv.rthost != null) {
};
}
let authMethod = 'default';
if (argv.noauth || argv.auth === 'none') {
authMethod = 'none';
}
const CONFIG = {
baseURI: argv.baseuri,
dbCleanInterval: argv.dbclean,
dbPath: path.resolve(path.join(argv.rundir, 'db')),
tempPath: path.resolve(path.join(argv.rundir, 'temp')),
disableUsersAndAuth: argv.noauth,
authMethod,
configUser: connectionSettings,
floodServerHost: argv.host,
floodServerPort: argv.port,

3
config.d.ts vendored
View File

@@ -1,3 +1,4 @@
import type {AuthMethod} from '@shared/schema/Auth';
import type {ClientConnectionSettings} from '@shared/schema/ClientConnectionSettings';
declare const CONFIG: {
@@ -5,7 +6,7 @@ declare const CONFIG: {
dbCleanInterval: number;
dbPath: string;
tempPath: string;
disableUsersAndAuth: boolean;
authMethod: AuthMethod;
configUser: ClientConnectionSettings;
floodServerHost: string;
floodServerPort: number;

View File

@@ -24,11 +24,23 @@ const CONFIG = {
// Where to store Flood's temporary files
tempPath: './run/temp/',
// If this is true, there will be no users and no attempt to
// authenticate or password-protect any page. In that case,
// instead of per-user config, the following configUser settings
// will be used.
disableUsersAndAuth: false,
//
// Authentication and user management method:
//
// default:
// Flood uses its own authentication and user management system. Users are authenticated
// by password and will be prompted to configure the connection to torrent client in the
// web interface. On successful authentication via /authenticate API endpoint, Flood will
// send a cookie with token to user. Users with admin privileges may create additional
// users with different password and torrent client configurations.
//
// none:
// There is no per-user config and no attempt to authenticate. An auth cookie with token is
// still needed to access API endpoints. This allows us to utilize browser's protections
// against session hijacking. The cookie with token will be sent unconditionally when
// /authenticate or /verify endpoints are accessed. Instead of per-user config, the
// configUser settings will be used.
authMethod: 'default',
// Settings for the no-user configuration.
configUser: {

View File

@@ -7,7 +7,7 @@ const temporaryRuntimeDirectory = path.resolve(os.tmpdir(), `flood.test.${crypto
process.argv = ['node', 'flood'];
process.argv.push('--rundir', temporaryRuntimeDirectory);
process.argv.push('--noauth', 'false');
process.argv.push('--auth', 'default');
afterAll(() => {
if (process.env.CI !== 'true') {

View File

@@ -21,7 +21,7 @@ const qBittorrentDaemon = spawn(
process.argv = ['node', 'flood'];
process.argv.push('--rundir', temporaryRuntimeDirectory);
process.argv.push('--noauth');
process.argv.push('--auth', 'none');
process.argv.push('--qburl', `http://127.0.0.1:${qbtPort}`);
process.argv.push('--qbuser', 'admin');
process.argv.push('--qbpass', 'adminadmin');

View File

@@ -32,7 +32,7 @@ const rTorrentProcess = spawn(
process.argv = ['node', 'flood'];
process.argv.push('--rundir', temporaryRuntimeDirectory);
process.argv.push('--noauth');
process.argv.push('--auth', 'none');
process.argv.push('--rtsocket', rTorrentSocket);
process.argv.push('--allowedpath', temporaryRuntimeDirectory);

View File

@@ -103,7 +103,7 @@ const startWebServer = () => {
console.log(chalk.green(`Flood server starting on ${address}.\n`));
if (config.disableUsersAndAuth) {
if (config.authMethod === 'none') {
console.log(chalk.yellow('Starting without builtin authentication\n'));
}
};

View File

@@ -169,7 +169,7 @@ class Users {
}
lookupUser(username: string, callback: (err: Error | null, user?: UserInDatabase) => void): void {
if (config.disableUsersAndAuth) {
if (config.authMethod === 'none') {
return callback(null, this.getConfigUser());
}
@@ -185,7 +185,7 @@ class Users {
}
listUsers(callback: (users: Array<UserInDatabase> | null, err?: Error) => void): void {
if (config.disableUsersAndAuth) {
if (config.authMethod === 'none') {
return callback([this.getConfigUser()]);
}

View File

@@ -81,7 +81,7 @@ const validationError = (res: Response, err: Error) => {
};
const preloadConfigs: AuthVerificationPreloadConfigs = {
disableAuth: config.disableUsersAndAuth,
authMethod: config.authMethod,
pollInterval: config.torrentClientPollInterval,
};
@@ -98,7 +98,7 @@ router.use('/users', passport.authenticate('jwt', {session: false}), requireAdmi
* @return {AuthAuthenticationResponse} 200 - success response - application/json
*/
router.post<unknown, unknown, AuthAuthenticationOptions>('/authenticate', (req, res) => {
if (config.disableUsersAndAuth) {
if (config.authMethod === 'none') {
sendAuthenticationResponse(res, Users.getConfigUser());
return;
}
@@ -163,8 +163,8 @@ router.use('/register', (req, res, next) => {
* @return {AuthAuthenticationResponse} 200 - success response - application/json
*/
router.post<unknown, unknown, AuthRegistrationOptions, {cookie: string}>('/register', (req, res) => {
// No user can be registered when disableUsersAndAuth is true
if (config.disableUsersAndAuth) {
// No user can be registered when authMethod is none
if (config.authMethod === 'none') {
// Return 404
res.status(404).send('Not found');
return;
@@ -200,7 +200,7 @@ router.post<unknown, unknown, AuthRegistrationOptions, {cookie: string}>('/regis
// Allow unauthenticated verification if no users are currently registered.
router.use('/verify', (req, res, next) => {
// Unconditionally provide a token if auth is disabled
if (config.disableUsersAndAuth) {
if (config.authMethod === 'none') {
const {username, level} = Users.getConfigUser();
getAuthToken(username, res);
@@ -284,8 +284,8 @@ router.get('/logout', (_req, res) => {
router.use('/', requireAdmin);
router.use('/users', (_req, res, next) => {
// No operation on user when disableUsersAndAuth is true
if (config.disableUsersAndAuth) {
// No operation on user when authMethod is none
if (config.authMethod === 'none') {
// Return 404
res.status(404).send('Not found');
}

View File

@@ -4,6 +4,8 @@ import type {infer as zodInfer} from 'zod';
import {AccessLevel} from './constants/Auth';
import {clientConnectionSettingsSchema} from './ClientConnectionSettings';
export type AuthMethod = 'default' | 'none';
export const credentialsSchema = object({
username: string(),
password: string(),

View File

@@ -3,6 +3,8 @@ import type {infer as zodInfer} from 'zod';
import {AccessLevel} from '../constants/Auth';
import {credentialsSchema} from '../Auth';
import type {AuthMethod} from '../Auth';
// All auth requests are schema validated to ensure security.
// POST /api/auth/authenticate
@@ -26,7 +28,7 @@ export type AuthUpdateUserOptions = zodInfer<typeof authUpdateUserSchema>;
// GET /api/auth/verify - preload configurations
export interface AuthVerificationPreloadConfigs {
disableAuth: boolean;
authMethod: AuthMethod;
pollInterval: number;
}