diff --git a/app/Auth/Permissions/PermissionsRepo.php b/app/Auth/Permissions/PermissionsRepo.php index 4d191679d..988146700 100644 --- a/app/Auth/Permissions/PermissionsRepo.php +++ b/app/Auth/Permissions/PermissionsRepo.php @@ -57,6 +57,7 @@ class PermissionsRepo public function saveNewRole(array $roleData): Role { $role = $this->role->newInstance($roleData); + $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : []; @@ -90,6 +91,7 @@ class PermissionsRepo $this->assignRolePermissions($role, $permissions); $role->fill($roleData); + $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true'; $role->save(); $this->permissionService->buildJointPermissionForRole($role); Activity::add(ActivityType::ROLE_UPDATE, $role); diff --git a/app/Auth/Role.php b/app/Auth/Role.php index 94ba39d1d..dcd960948 100644 --- a/app/Auth/Role.php +++ b/app/Auth/Role.php @@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; * @property string $description * @property string $external_auth_id * @property string $system_name + * @property bool $mfa_enforced */ class Role extends Model implements Loggable { diff --git a/app/Http/Middleware/EnforceMfaRequirements.php b/app/Http/Middleware/EnforceMfaRequirements.php new file mode 100644 index 000000000..957b42ae1 --- /dev/null +++ b/app/Http/Middleware/EnforceMfaRequirements.php @@ -0,0 +1,24 @@ +<?php + +namespace BookStack\Http\Middleware; + +use Closure; + +class EnforceMfaRequirements +{ + /** + * Handle an incoming request. + * + * @param \Illuminate\Http\Request $request + * @param \Closure $next + * @return mixed + */ + public function handle($request, Closure $next) + { + $mfaRequired = user()->roles()->where('mfa_enforced', '=', true)->exists(); + // TODO - Run this after auth (If authenticated) + // TODO - Redirect user to setup MFA or verify via MFA. + // TODO - Store mfa_pass into session for future requests? + return $next($request); + } +} diff --git a/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php b/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php new file mode 100644 index 000000000..c14d47ea7 --- /dev/null +++ b/database/migrations/2021_07_03_085038_add_mfa_enforced_to_roles_table.php @@ -0,0 +1,32 @@ +<?php + +use Illuminate\Database\Migrations\Migration; +use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\Schema; + +class AddMfaEnforcedToRolesTable extends Migration +{ + /** + * Run the migrations. + * + * @return void + */ + public function up() + { + Schema::table('roles', function (Blueprint $table) { + $table->boolean('mfa_enforced'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('roles', function (Blueprint $table) { + $table->dropColumn('mfa_enforced'); + }); + } +} diff --git a/resources/lang/en/settings.php b/resources/lang/en/settings.php index 789ef9d1b..d9b4854fe 100755 --- a/resources/lang/en/settings.php +++ b/resources/lang/en/settings.php @@ -138,6 +138,7 @@ return [ 'role_details' => 'Role Details', 'role_name' => 'Role Name', 'role_desc' => 'Short Description of Role', + 'role_mfa_enforced' => 'Requires Multi-Factor Authentication', 'role_external_auth_id' => 'External Authentication IDs', 'role_system' => 'System Permissions', 'role_manage_users' => 'Manage users', diff --git a/resources/views/settings/roles/form.blade.php b/resources/views/settings/roles/form.blade.php index 604acbb16..d1a61f0cd 100644 --- a/resources/views/settings/roles/form.blade.php +++ b/resources/views/settings/roles/form.blade.php @@ -11,13 +11,16 @@ </div> <div> <div class="form-group"> - <label for="name">{{ trans('settings.role_name') }}</label> + <label for="display_name">{{ trans('settings.role_name') }}</label> @include('form.text', ['name' => 'display_name']) </div> <div class="form-group"> - <label for="name">{{ trans('settings.role_desc') }}</label> + <label for="description">{{ trans('settings.role_desc') }}</label> @include('form.text', ['name' => 'description']) </div> + <div class="form-group"> + @include('form.checkbox', ['name' => 'mfa_enforced', 'label' => trans('settings.role_mfa_enforced') ]) + </div> @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2') <div class="form-group"> diff --git a/resources/views/settings/roles/index.blade.php b/resources/views/settings/roles/index.blade.php index 47cd8c920..898a96eef 100644 --- a/resources/views/settings/roles/index.blade.php +++ b/resources/views/settings/roles/index.blade.php @@ -27,7 +27,12 @@ @foreach($roles as $role) <tr> <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td> - <td>{{ $role->description }}</td> + <td> + @if($role->mfa_enforced) + <span title="{{ trans('settings.role_mfa_enforced') }}">@icon('lock') </span> + @endif + {{ $role->description }} + </td> <td class="text-center">{{ $role->users->count() }}</td> </tr> @endforeach diff --git a/routes/web.php b/routes/web.php index 7ab5890e0..3be6218b0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -224,6 +224,7 @@ Route::group(['middleware' => 'auth'], function () { Route::put('/roles/{id}', 'RoleController@update'); }); + // MFA Setup Routes Route::get('/mfa/setup', 'Auth\MfaController@setup'); Route::get('/mfa/totp-generate', 'Auth\MfaTotpController@generate'); Route::post('/mfa/totp-confirm', 'Auth\MfaTotpController@confirm'); diff --git a/tests/Permissions/RolesTest.php b/tests/Permissions/RolesTest.php index 09c3233e3..b9b1805b6 100644 --- a/tests/Permissions/RolesTest.php +++ b/tests/Permissions/RolesTest.php @@ -64,15 +64,16 @@ class RolesTest extends BrowserKitTest ->type('Test Role', 'display_name') ->type('A little test description', 'description') ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc]) + ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc, 'mfa_enforced' => false]) ->seePageIs('/settings/roles'); // Updating $this->asAdmin()->visit('/settings/roles') ->see($testRoleDesc) ->click($testRoleName) ->type($testRoleUpdateName, '#display_name') + ->check('#mfa_enforced') ->press('Save Role') - ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc]) + ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc, 'mfa_enforced' => true]) ->seePageIs('/settings/roles'); // Deleting $this->asAdmin()->visit('/settings/roles')