Skip to content

Commit 3ad1e90

Browse files
authored
Merge branch 'develop' into matthewpi/backups-patch-1
2 parents 00429c3 + f9ea96f commit 3ad1e90

File tree

10 files changed

+102
-43
lines changed

10 files changed

+102
-43
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Pterodactyl\Exceptions\Http;
4+
5+
use Throwable;
6+
use Illuminate\Http\Response;
7+
use Symfony\Component\HttpKernel\Exception\HttpException;
8+
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;
9+
10+
class TwoFactorAuthRequiredException extends HttpException implements HttpExceptionInterface
11+
{
12+
/**
13+
* TwoFactorAuthRequiredException constructor.
14+
*
15+
* @param \Throwable|null $previous
16+
*/
17+
public function __construct(Throwable $previous = null)
18+
{
19+
parent::__construct(Response::HTTP_BAD_REQUEST, "Two-factor authentication is required on this account in order to access this endpoint.", $previous);
20+
}
21+
}

app/Http/Kernel.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ class Kernel extends HttpKernel
8484
SubstituteClientApiBindings::class,
8585
'api..key:' . ApiKey::TYPE_ACCOUNT,
8686
AuthenticateIPAccess::class,
87+
// This is perhaps a little backwards with the Client API, but logically you'd be unable
88+
// to create/get an API key without first enabling 2FA on the account, so I suppose in the
89+
// end it makes sense.
90+
//
91+
// You just wouldn't be authenticating with the API by providing a 2FA token.
92+
RequireTwoFactorAuthentication::class,
8793
],
8894
'daemon' => [
8995
SubstituteBindings::class,
Lines changed: 26 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,12 @@
11
<?php
2-
/**
3-
* Pterodactyl - Panel
4-
* Copyright (c) 2015 - 2017 Dane Everitt <dane@daneeveritt.com>.
5-
*
6-
* This software is licensed under the terms of the MIT license.
7-
* https://opensource.org/licenses/MIT
8-
*/
92

103
namespace Pterodactyl\Http\Middleware;
114

125
use Closure;
136
use Illuminate\Support\Str;
147
use Illuminate\Http\Request;
158
use Prologue\Alerts\AlertsMessageBag;
9+
use Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException;
1610

1711
class RequireTwoFactorAuthentication
1812
{
@@ -30,7 +24,7 @@ class RequireTwoFactorAuthentication
3024
*
3125
* @var string
3226
*/
33-
protected $redirectRoute = 'account';
27+
protected $redirectRoute = '/account';
3428

3529
/**
3630
* RequireTwoFactorAuthentication constructor.
@@ -43,41 +37,46 @@ public function __construct(AlertsMessageBag $alert)
4337
}
4438

4539
/**
46-
* Handle an incoming request.
40+
* Check the user state on the incoming request to determine if they should be allowed to
41+
* proceed or not. This checks if the Panel is configured to require 2FA on an account in
42+
* order to perform actions. If so, we check the level at which it is required (all users
43+
* or just admins) and then check if the user has enabled it for their account.
4744
*
4845
* @param \Illuminate\Http\Request $request
4946
* @param \Closure $next
5047
* @return mixed
48+
*
49+
* @throws \Pterodactyl\Exceptions\Http\TwoFactorAuthRequiredException
5150
*/
5251
public function handle(Request $request, Closure $next)
5352
{
54-
if (! $request->user()) {
53+
/** @var \Pterodactyl\Models\User $user */
54+
$user = $request->user();
55+
$uri = rtrim($request->getRequestUri(), '/') . '/';
56+
$current = $request->route()->getName();
57+
58+
if (! $user || Str::startsWith($uri, ['/auth/']) || Str::startsWith($current, ['auth.', 'account.'])) {
5559
return $next($request);
5660
}
5761

58-
$current = $request->route()->getName();
59-
if (in_array($current, ['auth', 'account']) || Str::startsWith($current, ['auth.', 'account.'])) {
62+
$level = (int)config('pterodactyl.auth.2fa_required');
63+
// If this setting is not configured, or the user is already using 2FA then we can just
64+
// send them right through, nothing else needs to be checked.
65+
//
66+
// If the level is set as admin and the user is not an admin, pass them through as well.
67+
if ($level === self::LEVEL_NONE || $user->use_totp) {
68+
return $next($request);
69+
} else if ($level === self::LEVEL_ADMIN && ! $user->root_admin) {
6070
return $next($request);
6171
}
6272

63-
switch ((int) config('pterodactyl.auth.2fa_required')) {
64-
case self::LEVEL_ADMIN:
65-
if (! $request->user()->root_admin || $request->user()->use_totp) {
66-
return $next($request);
67-
}
68-
break;
69-
case self::LEVEL_ALL:
70-
if ($request->user()->use_totp) {
71-
return $next($request);
72-
}
73-
break;
74-
case self::LEVEL_NONE:
75-
default:
76-
return $next($request);
73+
// For API calls return an exception which gets rendered nicely in the API response.
74+
if ($request->isJson() || Str::startsWith($uri, '/api/')) {
75+
throw new TwoFactorAuthRequiredException;
7776
}
7877

7978
$this->alert->danger(trans('auth.2fa_must_be_enabled'))->flash();
8079

81-
return redirect()->route($this->redirectRoute);
80+
return redirect()->to($this->redirectRoute);
8281
}
8382
}

app/Transformers/Api/Application/ServerDatabaseTransformer.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
use Cake\Chronos\Chronos;
66
use Pterodactyl\Models\Database;
7-
use League\Fractal\Resource\Item;
87
use Pterodactyl\Models\DatabaseHost;
98
use Pterodactyl\Services\Acl\Api\AdminAcl;
109
use Illuminate\Contracts\Encryption\Encrypter;
@@ -72,7 +71,7 @@ public function transform(Database $model): array
7271
* @param \Pterodactyl\Models\Database $model
7372
* @return \League\Fractal\Resource\Item
7473
*/
75-
public function includePassword(Database $model): Item
74+
public function includePassword(Database $model)
7675
{
7776
return $this->item($model, function (Database $model) {
7877
return [

app/Transformers/Api/Client/DatabaseTransformer.php

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
namespace Pterodactyl\Transformers\Api\Client;
44

55
use Pterodactyl\Models\Database;
6-
use League\Fractal\Resource\Item;
76
use Pterodactyl\Models\Permission;
87
use Illuminate\Contracts\Encryption\Encrypter;
98
use Pterodactyl\Contracts\Extensions\HashidsInterface;
@@ -69,9 +68,9 @@ public function transform(Database $model): array
6968
* @param \Pterodactyl\Models\Database $database
7069
* @return \League\Fractal\Resource\Item|\League\Fractal\Resource\NullResource
7170
*/
72-
public function includePassword(Database $database): Item
71+
public function includePassword(Database $database)
7372
{
74-
if (!$this->getUser()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) {
73+
if (! $this->getUser()->can(Permission::ACTION_DATABASE_VIEW_PASSWORD, $database->server)) {
7574
return $this->null();
7675
}
7776

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import http from '@/api/http';
2+
import { AxiosError } from 'axios';
3+
import { History } from 'history';
4+
5+
export const setupInterceptors = (history: History) => {
6+
http.interceptors.response.use(resp => resp, (error: AxiosError) => {
7+
if (error.response?.status === 400) {
8+
if (error.response?.data.errors?.[0].code === 'TwoFactorAuthRequiredException') {
9+
if (!window.location.pathname.startsWith('/account')) {
10+
history.replace('/account', { twoFactorRedirect: true });
11+
}
12+
}
13+
}
14+
throw error;
15+
});
16+
};

resources/scripts/components/App.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useEffect } from 'react';
22
import ReactGA from 'react-ga';
33
import { hot } from 'react-hot-loader/root';
4-
import { BrowserRouter, Route, Switch, useLocation } from 'react-router-dom';
4+
import { Route, Router, Switch, useLocation } from 'react-router-dom';
55
import { StoreProvider } from 'easy-peasy';
66
import { store } from '@/state';
77
import DashboardRouter from '@/routers/DashboardRouter';
@@ -13,6 +13,8 @@ import ProgressBar from '@/components/elements/ProgressBar';
1313
import NotFound from '@/components/screens/NotFound';
1414
import tw from 'twin.macro';
1515
import GlobalStylesheet from '@/assets/css/GlobalStylesheet';
16+
import { createBrowserHistory } from 'history';
17+
import { setupInterceptors } from '@/api/interceptors';
1618

1719
interface ExtendedWindow extends Window {
1820
SiteConfiguration?: SiteSettings;
@@ -30,6 +32,10 @@ interface ExtendedWindow extends Window {
3032
};
3133
}
3234

35+
const history = createBrowserHistory({ basename: '/' });
36+
37+
setupInterceptors(history);
38+
3339
const Pageview = () => {
3440
const { pathname } = useLocation();
3541

@@ -72,15 +78,15 @@ const App = () => {
7278
<Provider store={store}>
7379
<ProgressBar/>
7480
<div css={tw`mx-auto w-auto`}>
75-
<BrowserRouter basename={'/'} key={'root-router'}>
81+
<Router history={history}>
7682
{SiteConfiguration?.analytics && <Pageview/>}
7783
<Switch>
7884
<Route path="/server/:id" component={ServerRouter}/>
7985
<Route path="/auth" component={AuthenticationRouter}/>
8086
<Route path="/" component={DashboardRouter}/>
8187
<Route path={'*'} component={NotFound}/>
8288
</Switch>
83-
</BrowserRouter>
89+
</Router>
8490
</div>
8591
</Provider>
8692
</StoreProvider>

resources/scripts/components/dashboard/AccountOverviewContainer.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ import PageContentBlock from '@/components/elements/PageContentBlock';
77
import tw from 'twin.macro';
88
import { breakpoint } from '@/theme';
99
import styled from 'styled-components/macro';
10+
import { RouteComponentProps } from 'react-router';
11+
import MessageBox from '@/components/MessageBox';
1012

1113
const Container = styled.div`
12-
${tw`flex flex-wrap my-10`};
14+
${tw`flex flex-wrap`};
1315
1416
& > div {
1517
${tw`w-full`};
@@ -24,10 +26,15 @@ const Container = styled.div`
2426
}
2527
`;
2628

27-
export default () => {
29+
export default ({ location: { state } }: RouteComponentProps) => {
2830
return (
2931
<PageContentBlock title={'Account Overview'}>
30-
<Container>
32+
{state?.twoFactorRedirect &&
33+
<MessageBox title={'2-Factor Required'} type={'error'}>
34+
Your account must have two-factor authentication enabled in order to continue.
35+
</MessageBox>
36+
}
37+
<Container css={[ tw`mb-10`, state?.twoFactorRedirect ? tw`mt-4` : tw`mt-10` ]}>
3138
<ContentBox title={'Update Password'} showFlashes={'account:password'}>
3239
<UpdatePasswordForm/>
3340
</ContentBox>

routes/api-client.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php
22

33
use Illuminate\Support\Facades\Route;
4+
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
45
use Pterodactyl\Http\Middleware\Api\Client\Server\SubuserBelongsToServer;
56
use Pterodactyl\Http\Middleware\Api\Client\Server\AuthenticateServerAccess;
67
use Pterodactyl\Http\Middleware\Api\Client\Server\AllocationBelongsToServer;
@@ -17,10 +18,10 @@
1718
Route::get('/permissions', 'ClientController@permissions');
1819

1920
Route::group(['prefix' => '/account'], function () {
20-
Route::get('/', 'AccountController@index')->name('api:client.account');
21-
Route::get('/two-factor', 'TwoFactorController@index');
22-
Route::post('/two-factor', 'TwoFactorController@store');
23-
Route::delete('/two-factor', 'TwoFactorController@delete');
21+
Route::get('/', 'AccountController@index')->name('api:client.account')->withoutMiddleware(RequireTwoFactorAuthentication::class);
22+
Route::get('/two-factor', 'TwoFactorController@index')->withoutMiddleware(RequireTwoFactorAuthentication::class);
23+
Route::post('/two-factor', 'TwoFactorController@store')->withoutMiddleware(RequireTwoFactorAuthentication::class);
24+
Route::delete('/two-factor', 'TwoFactorController@delete')->withoutMiddleware(RequireTwoFactorAuthentication::class);
2425

2526
Route::put('/email', 'AccountController@updateEmail')->name('api:client.account.update-email');
2627
Route::put('/password', 'AccountController@updatePassword')->name('api:client.account.update-password');

routes/base.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
<?php
22

3+
use Pterodactyl\Http\Middleware\RequireTwoFactorAuthentication;
4+
35
Route::get('/', 'IndexController@index')->name('index')->fallback();
4-
Route::get('/account', 'IndexController@index')->name('account');
6+
Route::get('/account', 'IndexController@index')
7+
->withoutMiddleware(RequireTwoFactorAuthentication::class)
8+
->name('account');
59

610
Route::get('/locales/{locale}/{namespace}.json', 'LocaleController')
11+
->withoutMiddleware(RequireTwoFactorAuthentication::class)
712
->where('namespace', '.*');
813

914
Route::get('/{react}', 'IndexController@index')

0 commit comments

Comments
 (0)