Added role based MFA control
- Added new DB column for control and role updated create/update actions. - Created new middleware as a start to actual enforcement logic. - Added indicator to role list of whether MFA is enforced.
This commit is contained in:
		
							parent
							
								
									529971c534
								
							
						
					
					
						commit
						09c2814dc7
					
				| 
						 | 
					@ -57,6 +57,7 @@ class PermissionsRepo
 | 
				
			||||||
    public function saveNewRole(array $roleData): Role
 | 
					    public function saveNewRole(array $roleData): Role
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        $role = $this->role->newInstance($roleData);
 | 
					        $role = $this->role->newInstance($roleData);
 | 
				
			||||||
 | 
					        $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
 | 
				
			||||||
        $role->save();
 | 
					        $role->save();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
 | 
					        $permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
 | 
				
			||||||
| 
						 | 
					@ -90,6 +91,7 @@ class PermissionsRepo
 | 
				
			||||||
        $this->assignRolePermissions($role, $permissions);
 | 
					        $this->assignRolePermissions($role, $permissions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        $role->fill($roleData);
 | 
					        $role->fill($roleData);
 | 
				
			||||||
 | 
					        $role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
 | 
				
			||||||
        $role->save();
 | 
					        $role->save();
 | 
				
			||||||
        $this->permissionService->buildJointPermissionForRole($role);
 | 
					        $this->permissionService->buildJointPermissionForRole($role);
 | 
				
			||||||
        Activity::add(ActivityType::ROLE_UPDATE, $role);
 | 
					        Activity::add(ActivityType::ROLE_UPDATE, $role);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,6 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
 | 
				
			||||||
 * @property string $description
 | 
					 * @property string $description
 | 
				
			||||||
 * @property string $external_auth_id
 | 
					 * @property string $external_auth_id
 | 
				
			||||||
 * @property string $system_name
 | 
					 * @property string $system_name
 | 
				
			||||||
 | 
					 * @property bool   $mfa_enforced
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
class Role extends Model implements Loggable
 | 
					class Role extends Model implements Loggable
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -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');
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -138,6 +138,7 @@ return [
 | 
				
			||||||
    'role_details' => 'Role Details',
 | 
					    'role_details' => 'Role Details',
 | 
				
			||||||
    'role_name' => 'Role Name',
 | 
					    'role_name' => 'Role Name',
 | 
				
			||||||
    'role_desc' => 'Short Description of Role',
 | 
					    'role_desc' => 'Short Description of Role',
 | 
				
			||||||
 | 
					    'role_mfa_enforced' => 'Requires Multi-Factor Authentication',
 | 
				
			||||||
    'role_external_auth_id' => 'External Authentication IDs',
 | 
					    'role_external_auth_id' => 'External Authentication IDs',
 | 
				
			||||||
    'role_system' => 'System Permissions',
 | 
					    'role_system' => 'System Permissions',
 | 
				
			||||||
    'role_manage_users' => 'Manage users',
 | 
					    'role_manage_users' => 'Manage users',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,13 +11,16 @@
 | 
				
			||||||
            </div>
 | 
					            </div>
 | 
				
			||||||
            <div>
 | 
					            <div>
 | 
				
			||||||
                <div class="form-group">
 | 
					                <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'])
 | 
					                    @include('form.text', ['name' => 'display_name'])
 | 
				
			||||||
                </div>
 | 
					                </div>
 | 
				
			||||||
                <div class="form-group">
 | 
					                <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'])
 | 
					                    @include('form.text', ['name' => 'description'])
 | 
				
			||||||
                </div>
 | 
					                </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')
 | 
					                @if(config('auth.method') === 'ldap' || config('auth.method') === 'saml2')
 | 
				
			||||||
                    <div class="form-group">
 | 
					                    <div class="form-group">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,7 +27,12 @@
 | 
				
			||||||
                @foreach($roles as $role)
 | 
					                @foreach($roles as $role)
 | 
				
			||||||
                    <tr>
 | 
					                    <tr>
 | 
				
			||||||
                        <td><a href="{{ url("/settings/roles/{$role->id}") }}">{{ $role->display_name }}</a></td>
 | 
					                        <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>
 | 
					                        <td class="text-center">{{ $role->users->count() }}</td>
 | 
				
			||||||
                    </tr>
 | 
					                    </tr>
 | 
				
			||||||
                @endforeach
 | 
					                @endforeach
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -224,6 +224,7 @@ Route::group(['middleware' => 'auth'], function () {
 | 
				
			||||||
        Route::put('/roles/{id}', 'RoleController@update');
 | 
					        Route::put('/roles/{id}', 'RoleController@update');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // MFA Setup Routes
 | 
				
			||||||
    Route::get('/mfa/setup', 'Auth\MfaController@setup');
 | 
					    Route::get('/mfa/setup', 'Auth\MfaController@setup');
 | 
				
			||||||
    Route::get('/mfa/totp-generate', 'Auth\MfaTotpController@generate');
 | 
					    Route::get('/mfa/totp-generate', 'Auth\MfaTotpController@generate');
 | 
				
			||||||
    Route::post('/mfa/totp-confirm', 'Auth\MfaTotpController@confirm');
 | 
					    Route::post('/mfa/totp-confirm', 'Auth\MfaTotpController@confirm');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -64,15 +64,16 @@ class RolesTest extends BrowserKitTest
 | 
				
			||||||
            ->type('Test Role', 'display_name')
 | 
					            ->type('Test Role', 'display_name')
 | 
				
			||||||
            ->type('A little test description', 'description')
 | 
					            ->type('A little test description', 'description')
 | 
				
			||||||
            ->press('Save Role')
 | 
					            ->press('Save Role')
 | 
				
			||||||
            ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc])
 | 
					            ->seeInDatabase('roles', ['display_name' => $testRoleName, 'description' => $testRoleDesc, 'mfa_enforced' => false])
 | 
				
			||||||
            ->seePageIs('/settings/roles');
 | 
					            ->seePageIs('/settings/roles');
 | 
				
			||||||
        // Updating
 | 
					        // Updating
 | 
				
			||||||
        $this->asAdmin()->visit('/settings/roles')
 | 
					        $this->asAdmin()->visit('/settings/roles')
 | 
				
			||||||
            ->see($testRoleDesc)
 | 
					            ->see($testRoleDesc)
 | 
				
			||||||
            ->click($testRoleName)
 | 
					            ->click($testRoleName)
 | 
				
			||||||
            ->type($testRoleUpdateName, '#display_name')
 | 
					            ->type($testRoleUpdateName, '#display_name')
 | 
				
			||||||
 | 
					            ->check('#mfa_enforced')
 | 
				
			||||||
            ->press('Save Role')
 | 
					            ->press('Save Role')
 | 
				
			||||||
            ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc])
 | 
					            ->seeInDatabase('roles', ['display_name' => $testRoleUpdateName, 'description' => $testRoleDesc, 'mfa_enforced' => true])
 | 
				
			||||||
            ->seePageIs('/settings/roles');
 | 
					            ->seePageIs('/settings/roles');
 | 
				
			||||||
        // Deleting
 | 
					        // Deleting
 | 
				
			||||||
        $this->asAdmin()->visit('/settings/roles')
 | 
					        $this->asAdmin()->visit('/settings/roles')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue