Implement RBAC (only built in roles) (#15212)

* Install bouncer

* Seeder and level migration

* Display and edit roles

* remove unused deluser page

* Update Radius and SSO to assign roles

* update AlertUtil direct level check to use roles instead

* rewrite ircbot auth handling

* Remove legacy auth getUserlist and getUserlevel methods, add getRoles
Set roles in LegacyUserProvider

* Small cleanups

* centralize role sync code
show roles on user preferences page

* VueSelect component WIP and a little docs

* WIP

* SelectControllers id and text fields.

* LibrenmsSelect component extracted from SettingSelectDynamic

* Handle multiple selections

* allow type coercion

* full width settings

* final style adjustments

* Final compiled assets update

* Style fixes

* Fix SSO tests

* Lint cleanups

* small style fix

* don't use json yet

* Update baseline for usptream package issues

* Change schema, not 100% sure it is correct
not sure why xor doesn't work
This commit is contained in:
Tony Murray
2023-08-28 00:13:40 -05:00
committed by GitHub
parent 4fc27d98e9
commit 2cd207028a
58 changed files with 1344 additions and 804 deletions

View File

@@ -0,0 +1,42 @@
<?php
/*
* RoleFactory.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2023 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace Database\Factories;
use Illuminate\Database\Eloquent\Factories\Factory;
use Silber\Bouncer\Database\Role;
class RoleFactory extends Factory
{
protected $model = Role::class;
public function definition()
{
return [
'name' => $this->faker->text(),
'title' => $this->faker->text(),
];
}
}

View File

@@ -2,10 +2,10 @@
namespace Database\Factories;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Silber\Bouncer\BouncerFacade as Bouncer;
/** @extends Factory<User> */
/** @extends Factory<\App\Models\User> */
class UserFactory extends Factory
{
/**
@@ -21,25 +21,23 @@ class UserFactory extends Factory
'realname' => $this->faker->name(),
'email' => $this->faker->safeEmail(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
'level' => 1,
];
}
public function admin()
{
return $this->state(function () {
return [
'level' => '10',
];
return $this->afterCreating(function ($user) {
Bouncer::allow('admin')->everything();
$user->assign('admin');
});
}
public function read()
{
return $this->state(function () {
return [
'level' => '5',
];
return $this->afterCreating(function ($user) {
Bouncer::allow(Bouncer::role()->firstOrCreate(['name' => 'global-read'], ['title' => 'Global Read']))
->to('viewAny', '*', []);
$user->assign('global-read');
});
}
}

View File

@@ -0,0 +1,100 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Silber\Bouncer\Database\Models;
class CreateBouncerTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (! Schema::hasTable('abilities')) {
Schema::create(Models::table('abilities'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('title')->nullable();
$table->bigInteger('entity_id')->unsigned()->nullable();
$table->string('entity_type')->nullable();
$table->boolean('only_owned')->default(false);
$table->longText('options')->nullable();
$table->integer('scope')->nullable()->index();
$table->timestamps();
});
}
if (! Schema::hasTable('roles')) {
Schema::create(Models::table('roles'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('title')->nullable();
$table->integer('scope')->nullable()->index();
$table->timestamps();
$table->unique(
['name', 'scope'],
'roles_name_unique'
);
});
}
if (! Schema::hasTable('assigned_roles')) {
Schema::create(Models::table('assigned_roles'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('role_id')->unsigned()->index();
$table->bigInteger('entity_id')->unsigned();
$table->string('entity_type');
$table->bigInteger('restricted_to_id')->unsigned()->nullable();
$table->string('restricted_to_type')->nullable();
$table->integer('scope')->nullable()->index();
$table->index(
['entity_id', 'entity_type', 'scope'],
'assigned_roles_entity_index'
);
$table->foreign('role_id')
->references('id')->on(Models::table('roles'))
->onUpdate('cascade')->onDelete('cascade');
});
}
if (! Schema::hasTable('permissions')) {
Schema::create(Models::table('permissions'), function (Blueprint $table) {
$table->bigIncrements('id');
$table->bigInteger('ability_id')->unsigned()->index();
$table->bigInteger('entity_id')->unsigned()->nullable();
$table->string('entity_type')->nullable();
$table->boolean('forbidden')->default(false);
$table->integer('scope')->nullable()->index();
$table->index(
['entity_id', 'entity_type', 'scope'],
'permissions_entity_index'
);
$table->foreign('ability_id')
->references('id')->on(Models::table('abilities'))
->onUpdate('cascade')->onDelete('cascade');
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop(Models::table('permissions'));
Schema::drop(Models::table('assigned_roles'));
Schema::drop(Models::table('roles'));
Schema::drop(Models::table('abilities'));
}
}

View File

@@ -0,0 +1,71 @@
<?php
use App\Models\User;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Silber\Bouncer\BouncerFacade as Bouncer;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
User::all()->each(function (User $user) {
$role = match ($user->getAttribute('level')) {
1 => 'user',
5 => 'global-read',
10 => 'admin',
default => null,
};
if ($role) {
Bouncer::assign($role)->to($user);
}
});
Bouncer::refresh(); // clear cache
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('level');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (! Schema::hasColumn('users', 'level')) {
Schema::table('users', function (Blueprint $table) {
$table->tinyInteger('level')->default(0)->after('descr');
});
}
User::whereIs('admin', 'global-read', 'user')->get()->each(function (User $user) {
$user->setAttribute('level', $this->getLevel($user));
$user->save();
});
Bouncer::refresh();
}
private function getLevel(User $user): int
{
if ($user->isA('admin')) {
return 10;
}
if ($user->isA('global-read')) {
return 7;
}
if ($user->isA('user')) {
return 1;
}
return 0;
}
};

View File

@@ -0,0 +1,41 @@
<?php
/**
* RolesSeeder.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2023 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Silber\Bouncer\BouncerFacade as Bouncer;
class RolesSeeder extends Seeder
{
public function run(): void
{
// set abilities for default rules
Bouncer::allow('admin')->everything();
Bouncer::allow(Bouncer::role()->firstOrCreate(['name' => 'global-read'], ['title' => 'Global Read']))
->to('viewAny', '*', []);
Bouncer::role()->firstOrCreate(['name' => 'user'], ['title' => 'User']);
}
}