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

@@ -20,13 +20,16 @@
namespace LibreNMS;
use LibreNMS\Authentication\LegacyAuth;
use App\Models\Device;
use App\Models\Eventlog;
use App\Models\Port;
use App\Models\Service;
use App\Models\User;
use LibreNMS\DB\Eloquent;
use LibreNMS\Enum\AlertState;
use LibreNMS\Util\Number;
use LibreNMS\Util\Time;
use LibreNMS\Util\Version;
use Permissions;
class IRCBot
{
@@ -657,18 +660,11 @@ class IRCBot
$this->log("HostAuth on irc matching $host to " . $this->getUserHost($this->data));
}
if (preg_match("/$host/", $this->getUserHost($this->data))) {
$user_id = LegacyAuth::get()->getUserid($nms_user);
$user = LegacyAuth::get()->getUser($user_id);
$this->user['name'] = $user['username'];
$this->user['id'] = $user_id;
$this->user['level'] = LegacyAuth::get()->getUserlevel($user['username']);
$user = User::firstWhere('username', $nms_user);
$this->user['user'] = $user;
$this->user['expire'] = (time() + ($this->config['irc_authtime'] * 3600));
if ($this->user['level'] < 5) {
$this->user['devices'] = Permissions::devicesForUser($this->user['id'])->toArray();
$this->user['ports'] = Permissions::portsForUser($this->user['id'])->toArray();
}
if ($this->debug) {
$this->log("HostAuth on irc for '" . $user['username'] . "', ID: '" . $user_id . "', Host: '" . $host);
$this->log("HostAuth on irc for '" . $user->username . "', ID: '" . $user->user_id . "', Host: '" . $host);
}
return true;
@@ -695,31 +691,22 @@ class IRCBot
if (strlen($params[0]) == 64) {
if ($this->tokens[$this->getUser($this->data)] == $params[0]) {
$this->user['expire'] = (time() + ($this->config['irc_authtime'] * 3600));
$tmp_user = LegacyAuth::get()->getUser($this->user['id']);
$tmp = LegacyAuth::get()->getUserlevel($tmp_user['username']);
$this->user['level'] = $tmp;
if ($this->user['level'] < 5) {
$this->user['devices'] = Permissions::devicesForUser($this->user['id'])->toArray();
$this->user['ports'] = Permissions::portsForUser($this->user['id'])->toArray();
}
return $this->respond('Authenticated.');
} else {
return $this->respond('Nope.');
}
} else {
$user_id = LegacyAuth::get()->getUserid($params[0]);
$user = LegacyAuth::get()->getUser($user_id);
if ($user['email'] && $user['username'] == $params[0]) {
$user = User::firstWhere('username', $params[0]);
if ($user->email && $user->username == $params[0]) {
$token = hash('gost', openssl_random_pseudo_bytes(1024));
$this->tokens[$this->getUser($this->data)] = $token;
$this->user['name'] = $params[0];
$this->user['id'] = $user['user_id'];
$this->user['user'] = $user;
if ($this->debug) {
$this->log("Auth for '" . $params[0] . "', ID: '" . $user['user_id'] . "', Token: '" . $token . "', Mail: '" . $user['email'] . "'");
$this->log("Auth for '" . $params[0] . "', ID: '" . $user->user_id . "', Token: '" . $token . "', Mail: '" . $user->email . "'");
}
if (send_mail($user['email'], 'LibreNMS IRC-Bot Authtoken', "Your Authtoken for the IRC-Bot:\r\n\r\n" . $token . "\r\n\r\n") === true) {
if (send_mail($user->email, 'LibreNMS IRC-Bot Authtoken', "Your Authtoken for the IRC-Bot:\r\n\r\n" . $token . "\r\n\r\n") === true) {
return $this->respond('Token sent!');
} else {
return $this->respond('Sorry, seems like mail doesnt like us.');
@@ -734,7 +721,7 @@ class IRCBot
private function _reload($params)
{
if ($this->user['level'] == 10) {
if ($this->user['user']->can('irc.reload')) {
if ($params == 'external') {
$this->respond('Reloading external scripts.');
@@ -756,7 +743,7 @@ class IRCBot
private function _join($params)
{
if ($this->user['level'] == 10) {
if ($this->user['user']->can('irc.join')) {
return $this->joinChan($params);
} else {
return $this->respond('Permission denied.');
@@ -767,7 +754,7 @@ class IRCBot
private function _quit($params)
{
if ($this->user['level'] == 10) {
if ($this->user['user']->can('irc.quit')) {
$this->ircRaw('QUIT :Requested');
return exit;
@@ -812,31 +799,30 @@ class IRCBot
if (strlen($params[1]) > 0) {
$hostname = preg_replace("/[^A-z0-9\.\-]/", '', $params[1]);
}
$hostname = $hostname . '%';
if ($this->user['level'] < 5) {
$tmp = dbFetchRows('SELECT `event_id`, eventlog.device_id, devices.hostname, `datetime`,`message`, eventlog.type FROM `eventlog`, `devices` WHERE eventlog.device_id=devices.device_id and devices.hostname like "' . $hostname . '" and eventlog.device_id IN (' . implode(',', $this->user['devices']) . ') ORDER BY `event_id` DESC LIMIT ' . (int) $num);
} else {
$tmp = dbFetchRows('SELECT `event_id`, eventlog.device_id, devices.hostname, `datetime`,`message`, eventlog.type FROM `eventlog`, `devices` WHERE eventlog.device_id=devices.device_id and devices.hostname like "' . $hostname . '" ORDER BY `event_id` DESC LIMIT ' . (int) $num);
}
$tmp = Eventlog::with('device')->hasAccess($this->user['user'])->whereIn('device_id', function ($query) use ($hostname) {
return $query->where('hostname', 'like', $hostname . '%')->select('device_id');
})->select(['event_id', 'datetime', 'type', 'message'])->orderBy('event_id')->limit((int) $num)->get();
/** @var Eventlog $logline */
foreach ($tmp as $logline) {
$response = $logline['datetime'] . ' ';
$response .= $this->_color($logline['hostname'], null, null, 'bold') . ' ';
$response = $logline->datetime . ' ';
$response .= $this->_color($logline->device->displayName(), null, null, 'bold') . ' ';
if ($this->config['irc_alert_utf8']) {
if (preg_match('/critical alert/', $logline['message'])) {
$response .= preg_replace('/critical alert/', $this->_color('critical alert', 'red'), $logline['message']) . ' ';
} elseif (preg_match('/warning alert/', $logline['message'])) {
$response .= preg_replace('/warning alert/', $this->_color('warning alert', 'yellow'), $logline['message']) . ' ';
} elseif (preg_match('/recovery/', $logline['message'])) {
$response .= preg_replace('/recovery/', $this->_color('recovery', 'green'), $logline['message']) . ' ';
if (preg_match('/critical alert/', $logline->message)) {
$response .= preg_replace('/critical alert/', $this->_color('critical alert', 'red'), $logline->message) . ' ';
} elseif (preg_match('/warning alert/', $logline->message)) {
$response .= preg_replace('/warning alert/', $this->_color('warning alert', 'yellow'), $logline->message) . ' ';
} elseif (preg_match('/recovery/', $logline->message)) {
$response .= preg_replace('/recovery/', $this->_color('recovery', 'green'), $logline->message) . ' ';
} else {
$response .= $logline['message'] . ' ';
$response .= $logline->message . ' ';
}
} else {
$response .= $logline['message'] . ' ';
$response .= $logline->message . ' ';
}
if ($logline['type'] != 'NULL') {
$response .= $logline['type'] . ' ';
if ($logline->type != 'NULL') {
$response .= $logline->type . ' ';
}
if ($this->config['irc_floodlimit'] > 100) {
$this->floodcount += strlen($response);
@@ -862,23 +848,12 @@ class IRCBot
private function _down($params)
{
if ($this->user['level'] < 5) {
$tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE status=0 AND `device_id` IN (' . implode(',', $this->user['devices']) . ')');
} else {
$tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE status=0');
}
$devices = Device::hasAccess($this->user['user'])->isDown()
->select(['device_id', 'hostname', 'sysName', 'display', 'ip'])->get();
$msg = '';
foreach ($tmp as $db) {
if ($db['hostname']) {
$msg .= ', ' . $db['hostname'];
}
}
$msg = $devices->map->displayName()->implode(', ');
$msg = substr($msg, 2);
$msg = $msg ? $msg : 'Nothing to show :)';
return $this->respond($msg);
return $this->respond($msg ?: 'Nothing to show :)');
}
//end _down()
@@ -887,20 +862,16 @@ class IRCBot
{
$params = explode(' ', $params);
$hostname = $params[0];
$device = dbFetchRow('SELECT * FROM `devices` WHERE `hostname` = ?', [$hostname]);
$device = Device::hasAccess($this->user['user'])->firstWhere('hostname', $hostname);
if (! $device) {
return $this->respond('Error: Bad or Missing hostname, use .listdevices to show all devices.');
}
if ($this->user['level'] < 5 && ! in_array($device['device_id'], $this->user['devices'])) {
return $this->respond('Error: Permission denied.');
}
$status = $device->status ? 'Up ' . Time::formatInterval($device->uptime) : 'Down';
$status .= $device->ignore ? '*Ignored*' : '';
$status .= $device->disabled ? '*Disabled*' : '';
$status = $device['status'] ? 'Up ' . Time::formatInterval($device['uptime']) : 'Down';
$status .= $device['ignore'] ? '*Ignored*' : '';
$status .= $device['disabled'] ? '*Disabled*' : '';
return $this->respond($device['os'] . ' ' . $device['version'] . ' ' . $device['features'] . ' ' . $status);
return $this->respond($device->displayName() . ': ' . $device->os . ' ' . $device->version . ' ' . $device->features . ' ' . $status);
}
//end _device()
@@ -914,10 +885,14 @@ class IRCBot
return $this->respond('Error: Missing hostname or ifname.');
}
$device = dbFetchRow('SELECT * FROM `devices` WHERE `hostname` = ?', [$hostname]);
$port = dbFetchRow('SELECT * FROM `ports` WHERE (`ifName` = ? OR `ifDescr` = ?) AND device_id = ?', [$ifname, $ifname, $device['device_id']]);
if ($this->user['level'] < 5 && ! in_array($port['port_id'], $this->user['ports']) && ! in_array($device['device_id'], $this->user['devices'])) {
return $this->respond('Error: Permission denied.');
$device = Device::hasAccess($this->user['user'])->firstWhere('hostname', $hostname);
if (! $device) {
return $this->respond('Error: Bad or Missing hostname, use .listdevices to show all devices.');
}
$port = $device->ports()->hasAccess($this->user['user'])->where('ifName', $ifname)->orWhere('ifDescr', $ifname);
if (! $port) {
return $this->respond('Error: Port not found or you do not have access.');
}
$bps_in = Number::formatSi($port['ifInOctets_rate'] * 8, 2, 3, 'bps');
@@ -932,21 +907,11 @@ class IRCBot
private function _listdevices($params)
{
if ($this->user['level'] < 5) {
$tmp = dbFetchRows('SELECT `hostname` FROM `devices` WHERE `device_id` IN (' . implode(',', $this->user['devices']) . ')');
} else {
$tmp = dbFetchRows('SELECT `hostname` FROM `devices`');
}
$devices = Device::hasAccess($this->user['user'])->pluck('hostname');
$msg = '';
foreach ($tmp as $device) {
$msg .= ', ' . $device['hostname'];
}
$msg = $devices->implode(', ');
$msg = substr($msg, 2);
$msg = $msg ? $msg : 'Nothing to show..?';
return $this->respond($msg);
return $this->respond($msg ?: 'Nothing to show..?');
}
//end _listdevices()
@@ -956,26 +921,15 @@ class IRCBot
$params = explode(' ', $params);
$statustype = $params[0];
$d_w = '';
$d_a = '';
$p_w = '';
$p_a = '';
if ($this->user['level'] < 5) {
$d_w = ' WHERE device_id IN (' . implode(',', $this->user['devices']) . ')';
$d_a = ' AND device_id IN (' . implode(',', $this->user['devices']) . ')';
$p_w = ' WHERE port_id IN (' . implode(',', $this->user['ports']) . ') OR device_id IN (' . implode(',', $this->user['devices']) . ')';
$p_a = ' AND (I.port_id IN (' . implode(',', $this->user['ports']) . ') OR I.device_id IN (' . implode(',', $this->user['devices']) . '))';
}
switch ($statustype) {
case 'devices':
case 'device':
case 'dev':
$devcount = dbFetchCell('SELECT count(*) FROM devices' . $d_w);
$devup = dbFetchCell("SELECT count(*) FROM devices WHERE status = '1' AND `ignore` = '0'" . $d_a);
$devdown = dbFetchCell("SELECT count(*) FROM devices WHERE status = '0' AND `ignore` = '0'" . $d_a);
$devign = dbFetchCell("SELECT count(*) FROM devices WHERE `ignore` = '1'" . $d_a);
$devdis = dbFetchCell("SELECT count(*) FROM devices WHERE `disabled` = '1'" . $d_a);
$devcount = Device::hasAccess($this->user['user'])->count();
$devup = Device::hasAccess($this->user['user'])->isUp()->count();
$devdown = Device::hasAccess($this->user['user'])->isDown()->count();
$devign = Device::hasAccess($this->user['user'])->isIgnored()->count();
$devdis = Device::hasAccess($this->user['user'])->isDisabled()->count();
if ($devup > 0) {
$devup = $this->_color($devup, 'green');
}
@@ -991,11 +945,13 @@ class IRCBot
case 'ports':
case 'port':
case 'prt':
$prtcount = dbFetchCell('SELECT count(*) FROM ports' . $p_w);
$prtup = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE I.ifOperStatus = 'up' AND I.ignore = '0' AND I.device_id = D.device_id AND D.ignore = '0'" . $p_a);
$prtdown = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE I.ifOperStatus = 'down' AND I.ifAdminStatus = 'up' AND I.ignore = '0' AND D.device_id = I.device_id AND D.ignore = '0'" . $p_a);
$prtsht = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE I.ifAdminStatus = 'down' AND I.ignore = '0' AND D.device_id = I.device_id AND D.ignore = '0'" . $p_a);
$prtign = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE D.device_id = I.device_id AND (I.ignore = '1' OR D.ignore = '1')" . $p_a);
$prtcount = Port::hasAccess($this->user['user'])->count();
$prtup = Port::hasAccess($this->user['user'])->isUp()->count();
$prtdown = Port::hasAccess($this->user['user'])->isDown()->whereHas('device', fn ($q) => $q->where('ignore', 0))->count();
$prtsht = Port::hasAccess($this->user['user'])->isShutdown()->whereHas('device', fn ($q) => $q->where('ignore', 0))->count();
$prtign = Port::hasAccess($this->user['user'])->where(function ($query) {
$query->isIgnored()->orWhereHas('device', fn ($q) => $q->where('ignore', 1));
})->count();
// $prterr = dbFetchCell("SELECT count(*) FROM ports AS I, devices AS D WHERE D.device_id = I.device_id AND (I.ignore = '0' OR D.ignore = '0') AND (I.ifInErrors_delta > '0' OR I.ifOutErrors_delta > '0')".$p_a);
if ($prtup > 0) {
$prtup = $this->_color($prtup, 'green');
@@ -1014,15 +970,16 @@ class IRCBot
case 'srv':
$status_counts = [];
$status_colors = [0 => 'green', 3 => 'lightblue', 1 => 'yellow', 2 => 'red'];
$srvcount = dbFetchCell('SELECT COUNT(*) FROM services' . $d_w);
$srvign = dbFetchCell('SELECT COUNT(*) FROM services WHERE service_ignore = 1' . $d_a);
$srvdis = dbFetchCell('SELECT COUNT(*) FROM services WHERE service_disabled = 1' . $d_a);
$service_status = dbFetchRows("SELECT `service_status`, COUNT(*) AS `count` FROM `services` WHERE `service_disabled`=0 AND `service_ignore`=0 $d_a GROUP BY `service_status`");
$service_status = array_column($service_status, 'count', 'service_status'); // key by status
$srvcount = Service::hasAccess($this->user['user'])->count();
$srvign = Service::hasAccess($this->user['user'])->isIgnored()->count();
$srvdis = Service::hasAccess($this->user['user'])->isDisabled()->count();
$service_status = Service::hasAccess($this->user['user'])->isActive()->groupBy('service_status')
->select('service_status', \DB::raw('count(*) as count'))->get()
->pluck('count', 'service_status');
foreach ($status_colors as $status => $color) {
if (isset($service_status[$status])) {
$status_counts[$status] = $this->_color($service_status[$status], $color);
if ($service_status->has($status)) {
$status_counts[$status] = $this->_color($service_status->get($status), $color);
$srvcount = $this->_color($srvcount, $color, null, 'bold'); // upgrade the main count color
} else {
$status_counts[$status] = 0;