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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1344 additions and 804 deletions

View File

@ -123,18 +123,18 @@ class AlertUtil
}
}
foreach ($users as $user) {
if (empty($user['email'])) {
if (empty($user->email)) {
continue; // no email, skip this user
}
if (empty($user['realname'])) {
$user['realname'] = $user['username'];
}
if (Config::get('alert.globals') && ($user['level'] >= 5 && $user['level'] < 10)) {
$contacts[$user['email']] = $user['realname'];
} elseif (Config::get('alert.admins') && $user['level'] == 10) {
$contacts[$user['email']] = $user['realname'];
} elseif (Config::get('alert.users') == true && in_array($user['user_id'], $uids)) {
$contacts[$user['email']] = $user['realname'];
$name = $user->realname ?: $user->username;
if (Config::get('alert.globals') && $user->hasGlobalRead()) {
$contacts[$user->email] = $name;
} elseif (Config::get('alert.admins') && $user->isAdmin()) {
$contacts[$user->email] = $name;
} elseif (Config::get('alert.users') && in_array($user['user_id'], $uids)) {
$contacts[$user->email] = $name;
}
}

View File

@ -3,6 +3,7 @@
namespace LibreNMS\Authentication;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Exceptions\LdapMissingException;
@ -92,14 +93,13 @@ class ADAuthorizationAuthorizer extends MysqlAuthorizer
return false;
}
public function getUserlevel($username)
public function getRoles(string $username): array
{
$userlevel = $this->authLdapSessionCacheGet('userlevel');
if ($userlevel) {
return $userlevel;
} else {
$userlevel = 0;
$roles = $this->authLdapSessionCacheGet('roles');
if ($roles !== null) {
return $roles;
}
$roles = [];
// Find all defined groups $username is in
$search = ldap_search(
@ -110,18 +110,25 @@ class ADAuthorizationAuthorizer extends MysqlAuthorizer
);
$entries = ldap_get_entries($this->ldap_connection, $search);
// Loop the list and find the highest level
// collect all roles
$auth_ad_groups = Config::get('auth_ad_groups');
foreach ($entries[0]['memberof'] as $entry) {
$group_cn = $this->getCn($entry);
$auth_ad_groups = Config::get('auth_ad_groups');
if ($auth_ad_groups[$group_cn]['level'] > $userlevel) {
$userlevel = $auth_ad_groups[$group_cn]['level'];
if (isset($auth_ad_groups[$group_cn]['roles']) && is_array($auth_ad_groups[$group_cn]['roles'])) {
$roles = array_merge($roles, $auth_ad_groups[$group_cn]['roles']);
} elseif (isset($auth_ad_groups[$group_cn]['level'])) {
$role = LegacyAuthLevel::tryFrom($auth_ad_groups[$group_cn]['level'])?->getName();
if ($role) {
$roles[] = $role;
}
}
}
$this->authLdapSessionCacheSet('userlevel', $userlevel);
$roles = array_unique($roles);
$this->authLdapSessionCacheSet('roles', $roles);
return $userlevel;
return $roles;
}
public function getUserid($username)

View File

@ -7,6 +7,7 @@
namespace LibreNMS\Authentication;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Exceptions\LdapMissingException;
@ -124,26 +125,33 @@ class ActiveDirectoryAuthorizer extends AuthorizerBase
return false;
}
public function getUserlevel($username)
public function getRoles(string $username): array
{
$userlevel = 0;
$roles = [];
if (! Config::get('auth_ad_require_groupmembership', true)) {
if (Config::get('auth_ad_global_read', false)) {
$userlevel = 5;
$roles[] = 'global-read';
}
}
// cycle through defined groups, test for memberOf-ship
foreach (Config::get('auth_ad_groups', []) as $group => $level) {
foreach (Config::get('auth_ad_groups', []) as $group => $data) {
try {
if ($this->userInGroup($username, $group)) {
$userlevel = max($userlevel, $level['level']);
if (isset($data['roles']) && is_array($data['roles'])) {
$roles = array_merge($roles, $data['roles']);
} elseif (isset($data['level'])) {
$role = LegacyAuthLevel::tryFrom($data['level'])?->getName();
if ($role) {
$roles[] = $role;
}
}
}
} catch (AuthenticationException $e) {
}
}
return $userlevel;
return array_unique($roles);
}
public function getUserid($username)

View File

@ -148,32 +148,6 @@ trait ActiveDirectoryCommon
return $ldap_groups;
}
public function getUserlist()
{
$connection = $this->getConnection();
$userlist = [];
$ldap_groups = $this->getGroupList();
foreach ($ldap_groups as $ldap_group) {
$search_filter = "(&(memberOf:1.2.840.113556.1.4.1941:=$ldap_group)(!(userAccountControl:1.2.840.113556.1.4.803:=2)))";
if (Config::get('auth_ad_user_filter')) {
$search_filter = '(&' . Config::get('auth_ad_user_filter') . $search_filter . ')';
}
$attributes = ['samaccountname', 'displayname', 'objectsid', 'mail'];
$search = ldap_search($connection, Config::get('auth_ad_base_dn'), $search_filter, $attributes);
$results = ldap_get_entries($connection, $search);
foreach ($results as $result) {
if (isset($result['samaccountname'][0])) {
$userlist[$result['samaccountname'][0]] = $this->userFromAd($result);
}
}
}
return array_values($userlist);
}
/**
* Generate a user array from an AD LDAP entry
* Must have the attributes: objectsid, samaccountname, displayname, mail
@ -191,7 +165,6 @@ trait ActiveDirectoryCommon
'realname' => $entry['displayname'][0],
'email' => isset($entry['mail'][0]) ? $entry['mail'][0] : null,
'descr' => '',
'level' => $this->getUserlevel($entry['samaccountname'][0]),
'can_modify_passwd' => 0,
];
}

View File

@ -45,29 +45,11 @@ abstract class AuthorizerBase implements Authorizer
return static::$HAS_AUTH_USERMANAGEMENT;
}
public function addUser($username, $password, $level = 0, $email = '', $realname = '', $can_modify_passwd = 0, $description = '')
{
//not supported by default
return false;
}
public function deleteUser($user_id)
{
//not supported by default
return false;
}
public function canUpdateUsers()
{
return static::$CAN_UPDATE_USER;
}
public function updateUser($user_id, $realname, $level, $can_modify_passwd, $email)
{
//not supported by default
return false;
}
public function authIsExternal()
{
return static::$AUTH_IS_EXTERNAL;
@ -77,4 +59,9 @@ abstract class AuthorizerBase implements Authorizer
{
return $_SERVER[Config::get('http_auth_header')] ?? $_SERVER['PHP_AUTH_USER'] ?? null;
}
public function getRoles(string $username): array
{
return []; // no roles by default
}
}

View File

@ -34,21 +34,6 @@ class HttpAuthAuthorizer extends MysqlAuthorizer
return false;
}
public function getUserlevel($username)
{
$user_level = parent::getUserlevel($username);
if ($user_level) {
return $user_level;
}
if (Config::has('http_auth_guest')) {
return parent::getUserlevel(Config::get('http_auth_guest'));
}
return 0;
}
public function getUserid($username)
{
$user_id = parent::getUserid($username);

View File

@ -26,6 +26,7 @@ namespace LibreNMS\Authentication;
use App\Models\User;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Exceptions\LdapMissingException;
@ -113,32 +114,38 @@ class LdapAuthorizationAuthorizer extends AuthorizerBase
return false;
}
public function getUserlevel($username)
public function getRoles(string $username): array
{
$userlevel = $this->authLdapSessionCacheGet('userlevel');
if ($userlevel) {
return $userlevel;
} else {
$userlevel = 0;
$roles = $this->authLdapSessionCacheGet('roles');
if ($roles !== null) {
return $roles;
}
$roles = [];
// Find all defined groups $username is in
$filter = '(&(|(cn=' . implode(')(cn=', array_keys(Config::get('auth_ldap_groups'))) . '))(' . Config::get('auth_ldap_groupmemberattr') . '=' . $this->getMembername($username) . '))';
$search = ldap_search($this->ldap_connection, Config::get('auth_ldap_groupbase'), $filter);
$entries = ldap_get_entries($this->ldap_connection, $search);
// Loop the list and find the highest level
$authLdapGroups = Config::get('auth_ldap_groups');
// Collect all roles
foreach ($entries as $entry) {
$groupname = $entry['cn'][0];
$authLdapGroups = Config::get('auth_ldap_groups');
if ($authLdapGroups[$groupname]['level'] > $userlevel) {
$userlevel = $authLdapGroups[$groupname]['level'];
if (isset($authLdapGroups[$groupname]['roles']) && is_array($authLdapGroups[$groupname]['roles'])) {
$roles = array_merge($roles, $authLdapGroups[$groupname]['roles']);
} elseif (isset($authLdapGroups[$groupname]['level'])) {
$role = LegacyAuthLevel::tryFrom($authLdapGroups[$groupname]['level'])?->getName();
if ($role) {
$roles[] = $role;
}
}
}
$this->authLdapSessionCacheSet('userlevel', $userlevel);
$roles = array_unique($roles);
$this->authLdapSessionCacheSet('roles', $roles);
return $userlevel;
return $roles;
}
public function getUserid($username)
@ -173,56 +180,38 @@ class LdapAuthorizationAuthorizer extends AuthorizerBase
return $user_id;
}
public function getUserlist()
public function getUser($user_id)
{
$userlist = [];
$filter = '(' . Config::get('auth_ldap_prefix') . '*)';
if (Config::get('auth_ldap_userlist_filter') != null) {
$filter = '(' . Config::get('auth_ldap_userlist_filter') . ')';
}
$uid_attr = strtolower(Config::get('auth_ldap_uid_attribute', 'uidnumber'));
$filter = "($uid_attr=$user_id)";
$search = ldap_search($this->ldap_connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
$entries = ldap_get_entries($this->ldap_connection, $search);
if ($entries['count']) {
foreach ($entries as $entry) {
$username = $entry['uid'][0];
$realname = $entry['cn'][0];
$user_id = $entry['uidnumber'][0];
$email = $entry[Config::get('auth_ldap_emailattr')][0];
$ldap_groups = $this->getGroupList();
foreach ($ldap_groups as $ldap_group) {
$ldap_comparison = ldap_compare(
$this->ldap_connection,
$ldap_group,
Config::get('auth_ldap_groupmemberattr'),
$this->getMembername($username)
);
if (! Config::has('auth_ldap_group') || $ldap_comparison === true) {
$userlist[] = [
'username' => $username,
'realname' => $realname,
'user_id' => $user_id,
'email' => $email,
];
}
$entry = $entries[0];
$username = $entry['uid'][0];
$realname = $entry['cn'][0];
$user_id = $entry['uidnumber'][0];
$email = $entry[Config::get('auth_ldap_emailattr')][0];
$ldap_groups = $this->getGroupList();
foreach ($ldap_groups as $ldap_group) {
$ldap_comparison = ldap_compare(
$this->ldap_connection,
$ldap_group,
Config::get('auth_ldap_groupmemberattr'),
$this->getMembername($username)
);
if (! Config::has('auth_ldap_group') || $ldap_comparison === true) {
return [
'username' => $username,
'realname' => $realname,
'user_id' => $user_id,
'email' => $email,
];
}
}
}
return $userlist;
}
public function getUser($user_id)
{
foreach ($this->getUserlist() as $user) {
if ($user['user_id'] == $user_id) {
$user['level'] = $this->getUserlevel($user['username']);
return $user;
}
}
return false;
}

View File

@ -4,6 +4,7 @@ namespace LibreNMS\Authentication;
use ErrorException;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Exceptions\LdapMissingException;
@ -101,10 +102,8 @@ class LdapAuthorizer extends AuthorizerBase
return false;
}
public function getUserlevel($username)
public function getRoles(string $username): array
{
$userlevel = 0;
try {
$connection = $this->getLdapConnection();
$groups = Config::get('auth_ldap_groups');
@ -126,18 +125,27 @@ class LdapAuthorizer extends AuthorizerBase
$search = ldap_search($connection, Config::get('auth_ldap_groupbase'), $filter);
$entries = ldap_get_entries($connection, $search);
// Loop the list and find the highest level
$roles = [];
// Collect all assigned roles
foreach ($entries as $entry) {
$groupname = $entry['cn'][0];
if ($groups[$groupname]['level'] > $userlevel) {
$userlevel = $groups[$groupname]['level'];
if (isset($groups[$groupname]['roles']) && is_array($groups[$groupname]['roles'])) {
$roles = array_merge($roles, $groups[$groupname]['roles']);
} elseif (isset($groups[$groupname]['level'])) {
$role = LegacyAuthLevel::tryFrom($groups[$groupname]['level'])?->getName();
if ($role) {
$roles[] = $role;
}
}
}
return array_unique($roles);
} catch (AuthenticationException $e) {
echo $e->getMessage() . PHP_EOL;
}
return $userlevel;
return [];
}
public function getUserid($username)
@ -161,65 +169,6 @@ class LdapAuthorizer extends AuthorizerBase
return -1;
}
public function getUserlist()
{
$userlist = [];
try {
$connection = $this->getLdapConnection();
$ldap_groups = $this->getGroupList();
if (empty($ldap_groups)) {
d_echo('No groups defined. Cannot search for users.');
return [];
}
$filter = '(' . Config::get('auth_ldap_prefix') . '*)';
if (Config::get('auth_ldap_userlist_filter') != null) {
$filter = '(' . Config::get('auth_ldap_userlist_filter') . ')';
}
// build group filter
$group_filter = '';
foreach ($ldap_groups as $group) {
$group_filter .= '(memberOf=' . trim($group) . ')';
}
if (count($ldap_groups) > 1) {
$group_filter = "(|$group_filter)";
}
// search using memberOf
$search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), "(&$filter$group_filter)");
if (ldap_count_entries($connection, $search)) {
foreach (ldap_get_entries($connection, $search) as $entry) {
$user = $this->ldapToUser($entry);
$userlist[$user['username']] = $user;
}
} else {
// probably doesn't support memberOf, go through all users, this could be slow
$search = ldap_search($connection, trim(Config::get('auth_ldap_suffix'), ','), $filter);
foreach (ldap_get_entries($connection, $search) as $entry) {
foreach ($ldap_groups as $ldap_group) {
if (ldap_compare(
$connection,
$ldap_group,
Config::get('auth_ldap_groupmemberattr', 'memberUid'),
$this->getMembername($entry['uid'][0])
)) {
$user = $this->ldapToUser($entry);
$userlist[$user['username']] = $user;
}
}
}
}
} catch (AuthenticationException $e) {
echo $e->getMessage() . PHP_EOL;
}
return $userlist;
}
public function getUser($user_id)
{
$connection = $this->getLdapConnection();
@ -362,7 +311,6 @@ class LdapAuthorizer extends AuthorizerBase
'realname' => $entry['cn'][0],
'user_id' => $entry[$uid_attr][0],
'email' => $entry[Config::get('auth_ldap_emailattr', 'mail')][0],
'level' => $this->getUserlevel($entry['uid'][0]),
];
}

View File

@ -4,7 +4,6 @@ namespace LibreNMS\Authentication;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use LibreNMS\DB\Eloquent;
use LibreNMS\Exceptions\AuthenticationException;
class MysqlAuthorizer extends AuthorizerBase
@ -55,71 +54,17 @@ class MysqlAuthorizer extends AuthorizerBase
}
}
public function addUser($username, $password, $level = 0, $email = '', $realname = '', $can_modify_passwd = 1, $descr = '')
{
$user_array = get_defined_vars();
// no nulls
$user_array = array_filter($user_array, function ($field) {
return ! is_null($field);
});
$new_user = User::thisAuth()->firstOrNew(['username' => $username], $user_array);
// only update new users
if (! $new_user->user_id) {
$new_user->auth_type = LegacyAuth::getType();
$new_user->setPassword($password);
$new_user->email = (string) $new_user->email;
$new_user->save();
$user_id = $new_user->user_id;
// set auth_id
$new_user->auth_id = (string) $this->getUserid($username);
$new_user->save();
if ($user_id) {
return $user_id;
}
}
return false;
}
public function userExists($username, $throw_exception = false)
{
return User::thisAuth()->where('username', $username)->exists();
}
public function getUserlevel($username)
{
return User::thisAuth()->where('username', $username)->value('level');
}
public function getUserid($username)
{
// for mysql user_id == auth_id
return User::thisAuth()->where('username', $username)->value('user_id');
}
public function deleteUser($user_id)
{
// could be used on cli, use Eloquent helper
Eloquent::DB()->table('bill_perms')->where('user_id', $user_id)->delete();
Eloquent::DB()->table('devices_perms')->where('user_id', $user_id)->delete();
Eloquent::DB()->table('devices_group_perms')->where('user_id', $user_id)->delete();
Eloquent::DB()->table('ports_perms')->where('user_id', $user_id)->delete();
Eloquent::DB()->table('users_prefs')->where('user_id', $user_id)->delete();
return (bool) User::destroy($user_id);
}
public function getUserlist()
{
return User::thisAuth()->orderBy('username')->get()->toArray();
}
public function getUser($user_id)
{
$user = User::find($user_id);
@ -129,16 +74,4 @@ class MysqlAuthorizer extends AuthorizerBase
return false;
}
public function updateUser($user_id, $realname, $level, $can_modify_passwd, $email)
{
$user = User::find($user_id);
$user->realname = $realname;
$user->level = (int) $level;
$user->can_modify_passwd = (int) $can_modify_passwd;
$user->email = $email;
return $user->save();
}
}

View File

@ -2,8 +2,12 @@
namespace LibreNMS\Authentication;
use App\Models\User;
use Dapphp\Radius\Radius;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Util\Debug;
@ -13,8 +17,9 @@ class RadiusAuthorizer extends MysqlAuthorizer
protected static $CAN_UPDATE_USER = true;
protected static $CAN_UPDATE_PASSWORDS = false;
/** @var Radius */
protected $radius;
protected Radius $radius;
private array $roles = []; // temp cache of roles
public function __construct()
{
@ -33,30 +38,35 @@ class RadiusAuthorizer extends MysqlAuthorizer
$password = $credentials['password'] ?? null;
if ($this->radius->accessRequest($credentials['username'], $password) === true) {
// attribute 11 is "Filter-Id", apply and enforce user role (level) if set
$user = User::thisAuth()->firstOrNew(['username' => $credentials['username']], [
'auth_type' => LegacyAuth::getType(),
'can_modify_passwd' => 0,
]);
$user->save();
$this->roles[$credentials['username']] = $this->getDefaultRoles();
// cache a single role from the Filter-ID attribute now because attributes are cleared every accessRequest
$filter_id_attribute = $this->radius->getAttribute(11);
$level = match ($filter_id_attribute) {
'librenms_role_admin' => 10,
'librenms_role_normal' => 1,
'librenms_role_global-read' => 5,
default => Config::get('radius.default_level', 1)
};
// if Filter-Id was given and the user exists, update the level
if ($filter_id_attribute && $this->userExists($credentials['username'])) {
$user = \App\Models\User::find($this->getUserid($credentials['username']));
$user->level = $level;
$user->save();
return true;
if ($filter_id_attribute && Str::startsWith($filter_id_attribute, 'librenms_role_')) {
$this->roles[$credentials['username']] = [substr($filter_id_attribute, 14)];
}
$this->addUser($credentials['username'], $password, $level, '', $credentials['username'], 0);
return true;
}
throw new AuthenticationException();
}
public function getRoles(string $username): array
{
return $this->roles[$username] ?? $this->getDefaultRoles();
}
private function getDefaultRoles(): array
{
// return roles or translate from the old radius.default_level
return Config::get('radius.default_roles')
?: Arr::wrap(LegacyAuthLevel::from(Config::get('radius.default_level') ?? 1)->getName());
}
}

View File

@ -25,7 +25,10 @@
namespace LibreNMS\Authentication;
use App\Models\User;
use Illuminate\Support\Arr;
use LibreNMS\Config;
use LibreNMS\Enum\LegacyAuthLevel;
use LibreNMS\Exceptions\AuthenticationException;
use LibreNMS\Exceptions\InvalidIpException;
use LibreNMS\Util\IP;
@ -46,19 +49,21 @@ class SSOAuthorizer extends MysqlAuthorizer
throw new AuthenticationException('\'sso.user_attr\' config setting was not found or was empty');
}
// Build the user's details from attributes
$email = $this->authSSOGetAttr(Config::get('sso.email_attr'));
$realname = $this->authSSOGetAttr(Config::get('sso.realname_attr'));
$description = $this->authSSOGetAttr(Config::get('sso.descr_attr'));
$can_modify_passwd = 0;
// User has already been approved by the authenticator so if automatic user create/update is enabled, do it
if (Config::get('sso.create_users') || Config::get('sso.update_users')) {
$user = User::thisAuth()->firstOrNew(['username' => $credentials['username']]);
$level = $this->authSSOCalculateLevel();
$create = ! $user->exists && Config::get('sso.create_users');
$update = $user->exists && Config::get('sso.update_users');
// User has already been approved by the authenicator so if automatic user create/update is enabled, do it
if (Config::get('sso.create_users') && ! $this->userExists($credentials['username'])) {
$this->addUser($credentials['username'], null, $level, $email, $realname, $can_modify_passwd, $description ? $description : 'SSO User');
} elseif (Config::get('sso.update_users') && $this->userExists($credentials['username'])) {
$this->updateUser($this->getUserid($credentials['username']), $realname, $level, $can_modify_passwd, $email);
if ($create || $update) {
$user->auth_type = LegacyAuth::getType();
$user->can_modify_passwd = 0;
$user->realname = $this->authSSOGetAttr(Config::get('sso.realname_attr'));
$user->email = $this->authSSOGetAttr(Config::get('sso.email_attr'));
$user->descr = $this->authSSOGetAttr(Config::get('sso.descr_attr')) ?: 'SSO User';
$user->save();
}
}
return true;
@ -147,15 +152,19 @@ class SSOAuthorizer extends MysqlAuthorizer
/**
* Calculate the privilege level to assign to a user based on the configuration and attributes supplied by the external authenticator.
* Returns an integer if the permission is found, or raises an AuthenticationException if the configuration is not valid.
* Converts the legacy level into a role
*
* @return int
* @param string $username
* @return array
*
* @throws AuthenticationException
*/
public function authSSOCalculateLevel()
public function getRoles(string $username): array
{
if (Config::get('sso.group_strategy') === 'attribute') {
if (Config::get('sso.level_attr')) {
if (is_numeric($this->authSSOGetAttr(Config::get('sso.level_attr')))) {
return (int) $this->authSSOGetAttr(Config::get('sso.level_attr'));
return Arr::wrap(LegacyAuthLevel::tryFrom((int) $this->authSSOGetAttr(Config::get('sso.level_attr')))?->getName());
} else {
throw new AuthenticationException('group assignment by attribute requested, but httpd is not setting the attribute to a number');
}
@ -164,13 +173,13 @@ class SSOAuthorizer extends MysqlAuthorizer
}
} elseif (Config::get('sso.group_strategy') === 'map') {
if (Config::get('sso.group_level_map') && is_array(Config::get('sso.group_level_map')) && Config::get('sso.group_delimiter') && Config::get('sso.group_attr')) {
return (int) $this->authSSOParseGroups();
return Arr::wrap(LegacyAuthLevel::tryFrom((int) $this->authSSOParseGroups())?->getName());
} else {
throw new AuthenticationException('group assignment by level map requested, but \'sso.group_level_map\', \'sso.group_attr\', or \'sso.group_delimiter\' are not set in your config');
}
} elseif (Config::get('sso.group_strategy') === 'static') {
if (Config::get('sso.static_level')) {
return (int) Config::get('sso.static_level');
return Arr::wrap(LegacyAuthLevel::tryFrom((int) Config::get('sso.static_level'))?->getName());
} else {
throw new AuthenticationException('group assignment by static level was requested, but \'sso.group_level_map\' was not set in your config');
}

View File

@ -0,0 +1,31 @@
<?php
namespace LibreNMS\Enum;
enum LegacyAuthLevel: int
{
case user = 1;
case global_read = 5;
case admin = 10;
case demo = 11;
public function fromName(string $name): ?LegacyAuthLevel
{
return match ($name) {
'admin' => LegacyAuthLevel::admin,
'user' => LegacyAuthLevel::user,
'global-read', 'global_read' => LegacyAuthLevel::global_read,
'demo' => LegacyAuthLevel::demo,
default => null
};
}
public function getName(): string
{
if ($this == LegacyAuthLevel::global_read) {
return 'global-read';
}
return $this->name;
}
}

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;

View File

@ -26,14 +26,6 @@ interface Authorizer
*/
public function userExists($username, $throw_exception = false);
/**
* Get the userlevel of $username
*
* @param string $username The username to check
* @return int
*/
public function getUserlevel($username);
/**
* Get the user_id of $username
*
@ -51,7 +43,6 @@ interface Authorizer
* realname
* email
* descr
* level
* can_modify_passwd
*
* @param int $user_id
@ -59,48 +50,6 @@ interface Authorizer
*/
public function getUser($user_id);
/**
* Add a new user.
*
* @param string $username
* @param string $password
* @param int $level
* @param string $email
* @param string $realname
* @param int $can_modify_passwd If this user is allowed to edit their password
* @param string $description
* @return int|false Returns the added user_id or false if adding failed
*/
public function addUser($username, $password, $level = 0, $email = '', $realname = '', $can_modify_passwd = 0, $description = '');
/**
* Update the some of the fields of a user
*
* @param int $user_id The user_id to update
* @param string $realname
* @param int $level
* @param int $can_modify_passwd
* @param string $email
* @return bool If the update was successful
*/
public function updateUser($user_id, $realname, $level, $can_modify_passwd, $email);
/**
* Delete a user.
*
* @param int $user_id
* @return bool If the deletion was successful
*/
public function deleteUser($user_id);
/**
* Get a list of all users in this Authorizer
* !Warning! this could be very slow for some Authorizer types or configurations
*
* @return array
*/
public function getUserlist();
/**
* Check if this Authorizer can add or remove users.
* You must also check canUpdateUsers() to see if it can edit users.
@ -140,4 +89,10 @@ interface Authorizer
* @return string|null
*/
public function getExternalUsername();
/**
* @param string $username
* @return string[] get a list of roles for the user, they need not exist ahead of time
*/
public function getRoles(string $username): array;
}

View File

@ -30,6 +30,7 @@ use App\Models\User;
use Illuminate\Validation\Rule;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
use Silber\Bouncer\Database\Role;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
@ -50,7 +51,7 @@ class AddUserCommand extends LnmsCommand
$this->addArgument('username', InputArgument::REQUIRED);
$this->addOption('password', 'p', InputOption::VALUE_REQUIRED);
$this->addOption('role', 'r', InputOption::VALUE_REQUIRED, __('commands.user:add.options.role', ['roles' => '[normal, global-read, admin]']), 'normal');
$this->addOption('role', 'r', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, __('commands.user:add.options.role', ['roles' => '[user, global-read, admin]']), ['user']);
$this->addOption('email', 'e', InputOption::VALUE_REQUIRED);
$this->addOption('full-name', 'l', InputOption::VALUE_REQUIRED);
$this->addOption('descr', 's', InputOption::VALUE_REQUIRED);
@ -67,16 +68,12 @@ class AddUserCommand extends LnmsCommand
$this->warn(__('commands.user:add.wrong-auth'));
}
$roles = [
'normal' => 1,
'global-read' => 5,
'admin' => 10,
];
$roles = Role::pluck('name');
$this->validate([
'username' => ['required', Rule::unique('users', 'username')->where('auth_type', 'mysql')],
'email' => 'nullable|email',
'role' => Rule::in(array_keys($roles)),
'role' => Rule::in($roles->keys()),
]);
// set get password
@ -87,7 +84,6 @@ class AddUserCommand extends LnmsCommand
$user = new User([
'username' => $this->argument('username'),
'level' => $roles[$this->option('role')],
'descr' => $this->option('descr'),
'email' => $this->option('email'),
'realname' => $this->option('full-name'),
@ -96,6 +92,7 @@ class AddUserCommand extends LnmsCommand
$user->setPassword($password);
$user->save();
$user->allow($this->option('role'));
$user->auth_id = (string) LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$user->save();

View File

@ -30,6 +30,7 @@ use Illuminate\Database\QueryException;
use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use LibreNMS\Interfaces\InstallerStep;
use Silber\Bouncer\BouncerFacade as Bouncer;
class MakeUserController extends InstallationController implements InstallerStep
{
@ -72,10 +73,12 @@ class MakeUserController extends InstallationController implements InstallerStep
if (! $this->complete()) {
$this->configureDatabase();
$user = new User($request->only(['username', 'password', 'email']));
$user->level = 10; // admin
$user->setPassword($request->get('password'));
$res = $user->save();
Bouncer::allow('admin')->everything(); // make sure admin role exists
$user->assign('admin');
if ($res) {
$message = trans('install.user.success');
$this->markStepComplete();

View File

@ -0,0 +1,46 @@
<?php
/*
* RolesController.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 App\Http\Controllers\Select;
use Illuminate\Http\Request;
use Silber\Bouncer\BouncerFacade as Bouncer;
class RoleController extends SelectController
{
protected ?string $idField = 'name';
protected ?string $textField = 'title';
protected function searchFields(Request $request)
{
return ['name'];
}
protected function baseQuery(Request $request)
{
return Bouncer::role()
->whereRaw('1 = ' . ((int) $request->user()->can('viewAny', Bouncer::role())));
}
}

View File

@ -27,13 +27,18 @@ namespace App\Http\Controllers\Select;
use App\Http\Controllers\PaginatedAjaxController;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
abstract class SelectController extends PaginatedAjaxController
{
protected ?string $idField = null;
protected ?string $textField = null;
final protected function baseRules()
{
return [
@ -54,9 +59,11 @@ abstract class SelectController extends PaginatedAjaxController
$this->validate($request, $this->rules());
$limit = $request->get('limit', 50);
$query = $this->baseQuery($request)->when($request->has('id'), function ($query) {
return $query->whereKey(request('id'));
});
$query = $this->baseQuery($request);
if ($this->idField && $this->textField) {
$query->select([$this->idField, $this->textField]);
}
$this->filterById($query, $request->get('id'));
$this->filter($request, $query, $this->filterFields($request));
$this->search($request->get('term'), $query, $this->searchFields($request));
$this->sort($request, $query);
@ -88,6 +95,14 @@ abstract class SelectController extends PaginatedAjaxController
*/
public function formatItem($model)
{
if ($this->idField && $this->textField) {
return [
'id' => $model->getAttribute($this->idField),
'text' => $model->getAttribute($this->textField),
];
}
// guess
$attributes = collect($model->getAttributes());
return [
@ -106,4 +121,21 @@ abstract class SelectController extends PaginatedAjaxController
return true;
}
protected function filterById(EloquentBuilder|Builder $query, ?string $id): EloquentBuilder|Builder
{
if ($id) {
// multiple
if (str_contains($id, ',')) {
$keys = explode(',', $id);
return $this->idField ? $query->whereIn($this->idField, $keys) : $query->whereKey($keys);
}
// use id field if given
return $this->idField ? $query->where($this->idField, $id) : $query->whereKey($id);
}
return $query;
}
}

View File

@ -74,7 +74,7 @@ class UserController extends Controller
$this->authorize('create', User::class);
$tmp_user = new User;
$tmp_user->can_modify_passwd = (int) LegacyAuth::get()->canUpdatePasswords(); // default to true for new users
$tmp_user->can_modify_passwd = LegacyAuth::getType() == 'mysql' ? 1 : 0; // default to true mysql
return view('user.create', [
'user' => $tmp_user,
@ -92,13 +92,14 @@ class UserController extends Controller
*/
public function store(StoreUserRequest $request, FlasherInterface $flasher)
{
$user = $request->only(['username', 'realname', 'email', 'descr', 'level', 'can_modify_passwd']);
$user = $request->only(['username', 'realname', 'email', 'descr', 'can_modify_passwd']);
$user['auth_type'] = LegacyAuth::getType();
$user['can_modify_passwd'] = $request->get('can_modify_passwd'); // checkboxes are missing when unchecked
$user = User::create($user);
$user->setPassword($request->new_password);
$user->setRoles($request->get('roles', []));
$user->auth_id = (string) LegacyAuth::get()->getUserid($user->username) ?: $user->user_id;
$this->updateDashboard($user, $request->get('dashboard'));
$this->updateTimezone($user, $request->get('timezone'));
@ -184,6 +185,7 @@ class UserController extends Controller
}
$user->fill($request->validated());
$user->setRoles($request->get('roles', []));
if ($request->has('dashboard') && $this->updateDashboard($user, $request->get('dashboard'))) {
$flasher->addSuccess(__('Updated dashboard for :username', ['username' => $user->username]));

View File

@ -7,6 +7,7 @@ use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use LibreNMS\Authentication\LegacyAuth;
use LibreNMS\Config;
use Silber\Bouncer\BouncerFacade as Bouncer;
class StoreUserRequest extends FormRequest
{
@ -17,7 +18,15 @@ class StoreUserRequest extends FormRequest
*/
public function authorize(): bool
{
return $this->user()->can('create', User::class);
if ($this->user()->can('create', User::class)) {
if ($this->user()->cannot('manage', Bouncer::role())) {
unset($this['roles']);
}
return true;
}
return false;
}
/**
@ -37,7 +46,8 @@ class StoreUserRequest extends FormRequest
'realname' => 'nullable|max:64|alpha_space',
'email' => 'nullable|email|max:64',
'descr' => 'nullable|max:30|alpha_space',
'level' => 'int',
'roles' => 'array',
'roles.*' => Rule::in(Bouncer::role()->pluck('name')),
'new_password' => 'required|confirmed|min:' . Config::get('password.min_length', 8),
'dashboard' => 'int',
];

View File

@ -2,9 +2,12 @@
namespace App\Http\Requests;
use App\Models\User;
use Hash;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use LibreNMS\Config;
use Silber\Bouncer\BouncerFacade as Bouncer;
class UpdateUserRequest extends FormRequest
{
@ -15,14 +18,17 @@ class UpdateUserRequest extends FormRequest
*/
public function authorize(): bool
{
if ($this->user()->isAdmin()) {
return true;
}
/** @var User|null $user */
$user = $this->route('user');
if ($user && $this->user()->can('update', $user)) {
// normal users cannot edit their level or ability to modify a password
unset($this['level'], $this['can_modify_passwd']);
// normal users cannot update their roles or ability to modify a password
if ($this->user()->cannot('manage', Bouncer::role())) {
unset($this['roles']);
}
if ($user->is($this->user())) {
unset($this['can_modify_passwd']);
}
return true;
}
@ -37,7 +43,7 @@ class UpdateUserRequest extends FormRequest
*/
public function rules(): array
{
if ($this->user()->isAdmin()) {
if ($this->user()->can('update', User::class)) {
return [
'realname' => 'nullable|max:64|alpha_space',
'email' => 'nullable|email|max:64',
@ -45,7 +51,8 @@ class UpdateUserRequest extends FormRequest
'new_password' => 'nullable|confirmed|min:' . Config::get('password.min_length', 8),
'new_password_confirmation' => 'nullable|same:new_password',
'dashboard' => 'int',
'level' => 'int',
'roles' => 'array',
'roles.*' => Rule::in(Bouncer::role()->pluck('name')),
'enabled' => 'nullable',
'can_modify_passwd' => 'nullable',
];
@ -72,7 +79,8 @@ class UpdateUserRequest extends FormRequest
{
$validator->after(function ($validator) {
// if not an admin and new_password is set, check old password matches
if (! $this->user()->isAdmin()) {
$user = $this->route('user');
if ($user && $this->user()->can('update', $user) && $this->user()->is($user)) {
if ($this->has('new_password')) {
if ($this->has('old_password')) {
$user = $this->route('user');

View File

@ -27,6 +27,18 @@ class Service extends DeviceRelatedModel
// ---- Query Scopes ----
/**
* @param Builder $query
* @return Builder
*/
public function scopeIsActive($query)
{
return $query->where([
['service_ignore', '=', 0],
['service_disabled', '=', 0],
]);
}
/**
* @param Builder $query
* @return Builder

View File

@ -13,16 +13,18 @@ use Illuminate\Support\Facades\Hash;
use LibreNMS\Authentication\LegacyAuth;
use NotificationChannels\WebPush\HasPushSubscriptions;
use Permissions;
use Silber\Bouncer\BouncerFacade as Bouncer;
use Silber\Bouncer\Database\HasRolesAndAbilities;
/**
* @method static \Database\Factories\UserFactory factory(...$parameters)
*/
class User extends Authenticatable
{
use Notifiable, HasFactory, HasPushSubscriptions;
use HasRolesAndAbilities, Notifiable, HasFactory, HasPushSubscriptions;
protected $primaryKey = 'user_id';
protected $fillable = ['realname', 'username', 'email', 'level', 'descr', 'can_modify_passwd', 'auth_type', 'auth_id', 'enabled'];
protected $fillable = ['realname', 'username', 'email', 'descr', 'can_modify_passwd', 'auth_type', 'auth_id', 'enabled'];
protected $hidden = ['password', 'remember_token', 'pivot'];
protected $attributes = [ // default values
'descr' => '',
@ -42,31 +44,29 @@ class User extends Authenticatable
public function toFlare(): array
{
return $this->only(['level', 'auth_type', 'enabled']);
return $this->only(['auth_type', 'enabled']);
}
// ---- Helper Functions ----
/**
* Test if this user has global read access
* these users have a level of 5, 10 or 11 (demo).
*
* @return bool
*/
public function hasGlobalRead()
{
return $this->hasGlobalAdmin() || $this->level == 5;
return $this->isA('admin', 'global-read');
}
/**
* Test if this user has global admin access
* these users have a level of 10 or 11 (demo).
*
* @return bool
*/
public function hasGlobalAdmin()
{
return $this->level >= 10;
return $this->isA('admin', 'demo');
}
/**
@ -76,7 +76,7 @@ class User extends Authenticatable
*/
public function isAdmin()
{
return $this->level == 10;
return $this->isA('admin');
}
/**
@ -86,7 +86,7 @@ class User extends Authenticatable
*/
public function isDemo()
{
return $this->level == 11;
return $this->isA('demo');
}
/**
@ -110,6 +110,20 @@ class User extends Authenticatable
$this->attributes['password'] = $password ? Hash::make($password) : null;
}
/**
* Set roles and remove extra roles, optionally creating non-existent roles, flush permissions cache for this user if roles changed
*/
public function setRoles(array $roles, bool $create = false): void
{
if ($roles != $this->getRoles()) {
if ($create) {
$this->assign($roles);
}
Bouncer::sync($this)->roles($roles);
Bouncer::refresh($this);
}
}
/**
* Check if the given user can set the password for this user
*
@ -167,7 +181,7 @@ class User extends Authenticatable
public function scopeAdminOnly($query)
{
$query->where('level', 10);
$query->whereIs('admin');
}
// ---- Accessors/Mutators ----

View File

@ -9,35 +9,15 @@ class UserPolicy
{
use HandlesAuthorization;
/**
* Determine whether the user can manage users.
*
* @param User $user
*/
public function manage(User $user): bool
{
return $user->isAdmin();
}
/**
* Determine whether the user can view the user.
*
* @param User $user
* @param User $target
*/
public function view(User $user, User $target): bool
public function view(User $user, User $target): ?bool
{
return $user->isAdmin() || $target->is($user);
}
/**
* Determine whether the user can view any user.
*
* @param User $user
*/
public function viewAny(User $user): bool
{
return $user->isAdmin();
return $target->is($user) ?: null; // allow users to view themselves
}
/**
@ -45,9 +25,14 @@ class UserPolicy
*
* @param User $user
*/
public function create(User $user): bool
public function create(User $user): ?bool
{
return $user->isAdmin();
// if not mysql, forbid, otherwise defer to bouncer
if (\LibreNMS\Config::get('auth_mechanism') != 'mysql') {
return false;
}
return null;
}
/**
@ -56,9 +41,13 @@ class UserPolicy
* @param User $user
* @param User $target
*/
public function update(User $user, User $target): bool
public function update(User $user, User $target = null): ?bool
{
return $user->isAdmin() || $target->is($user);
if ($target == null) {
return null;
}
return $target->is($user) ?: null; // allow user to update self or defer to bouncer
}
/**
@ -67,8 +56,8 @@ class UserPolicy
* @param User $user
* @param User $target
*/
public function delete(User $user, User $target): bool
public function delete(User $user, User $target): ?bool
{
return $user->isAdmin();
return $target->is($user) ? false : null; // do not allow users to delete themselves or defer to bouncer
}
}

View File

@ -6,6 +6,7 @@ use App\Guards\ApiTokenGuard;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
use Silber\Bouncer\BouncerFacade as Bouncer;
class AuthServiceProvider extends ServiceProvider
{
@ -31,6 +32,8 @@ class AuthServiceProvider extends ServiceProvider
*/
public function boot(): void
{
Bouncer::cache();
Auth::provider('legacy', function ($app, array $config) {
return new LegacyUserProvider();
});

View File

@ -208,6 +208,9 @@ class LegacyUserProvider implements UserProvider
$user->auth_id = (string) $auth_id;
$user->save();
// create and update roles
$user->setRoles($auth->getRoles($user->username), true);
return $user;
}
}

View File

@ -48,6 +48,7 @@
"php-flasher/flasher-laravel": "^1.12",
"phpmailer/phpmailer": "~6.0",
"predis/predis": "^2.0",
"silber/bouncer": "^1.0",
"socialiteproviders/manager": "^4.3",
"spatie/laravel-ignition": "^2.0",
"symfony/yaml": "^6.2",

77
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "ce02f0a79191fafde1a88553b37d0b4e",
"content-hash": "21dbcfec63eafb1ae9172473314a57f8",
"packages": [
{
"name": "amenadiel/jpgraph",
@ -5169,6 +5169,81 @@
],
"time": "2023-04-15T23:01:58+00:00"
},
{
"name": "silber/bouncer",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/JosephSilber/bouncer.git",
"reference": "502221b6724fe806aa01ffe08070edaa10222101"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JosephSilber/bouncer/zipball/502221b6724fe806aa01ffe08070edaa10222101",
"reference": "502221b6724fe806aa01ffe08070edaa10222101",
"shasum": ""
},
"require": {
"illuminate/auth": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/cache": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/container": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0",
"php": "^7.2|^8.0"
},
"require-dev": {
"illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0",
"illuminate/events": "^6.0|^7.0|^8.0|^9.0|^10.0",
"larapack/dd": "^1.1",
"mockery/mockery": "^1.3.3",
"phpunit/phpunit": "^8.0|^9.0"
},
"suggest": {
"illuminate/console": "Allows running the bouncer:clean artisan command",
"illuminate/events": "Required for multi-tenancy support"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Silber\\Bouncer\\BouncerServiceProvider"
],
"aliases": {
"Bouncer": "Silber\\Bouncer\\BouncerFacade"
}
}
},
"autoload": {
"psr-4": {
"Silber\\Bouncer\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joseph Silber",
"email": "contact@josephsilber.com"
}
],
"description": "Eloquent roles and abilities.",
"keywords": [
"abilities",
"acl",
"capabilities",
"eloquent",
"laravel",
"permissions",
"roles"
],
"support": {
"issues": "https://github.com/JosephSilber/bouncer/issues",
"source": "https://github.com/JosephSilber/bouncer/tree/v1.0.1"
},
"time": "2023-02-10T16:47:25+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.3.0",

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']);
}
}

View File

@ -291,23 +291,23 @@ setsebool -P httpd_can_connect_ldap 1
## Radius Authentication
Please note that a mysql user is created for each user the logs in
successfully. User level 1 is assigned by default to those accounts
unless radius sends a reply attribute with the correct userlevel.
successfully. Users are assigned the `user` role by default,
unless radius sends a reply attribute with a role.
You can change the default userlevel by setting
`radius.userlevel` to something other than 1.
You can change the default role(s) by setting
!!! setting "auth/radius"
```bash
lnms config:set radius.default_roles '["csr"]'
```
The attribute `Filter-ID` is a standard Radius-Reply-Attribute (string) that
can be assigned a value which translates into a userlevel in LibreNMS.
can be assigned a specially formatted string to assign a single role to the user.
The strings to send in `Filter-ID` reply attribute is *one* of the following:
- `librenms_role_normal` - Sets the value `1`, which is the normal user level.
- `librenms_role_admin` - Sets the value `5`, which is the administrator level.
- `librenms_role_global-read` - Sets the value `10`, which is the global read level.
The string to send in `Filter-ID` reply attribute must start with `librenms_role_` followed by the role name.
For example to set the admin role send `librenms_role_admin`
LibreNMS will ignore any other strings sent in `Filter-ID` and revert to default
userlevel that is set in your config.
role that is set in your config.
```php
$config['radius']['hostname'] = 'localhost';
@ -408,9 +408,9 @@ $config['auth_mechanism'] = 'ldap-authorization';
$config['auth_ldap_server'] = 'ldap.example.com'; // Set server(s), space separated. Prefix with ldaps:// for ssl
$config['auth_ldap_suffix'] = ',ou=People,dc=example,dc=com'; // appended to usernames
$config['auth_ldap_groupbase'] = 'ou=groups,dc=example,dc=com'; // all groups must be inside this
$config['auth_ldap_groups']['admin']['level'] = 10; // set admin group to admin level
$config['auth_ldap_groups']['pfy']['level'] = 5; // set pfy group to global read only level
$config['auth_ldap_groups']['support']['level'] = 1; // set support group as a normal user
$config['auth_ldap_groups']['admin']['roles'] = ['admin']; // set admin group to admin role
$config['auth_ldap_groups']['pfy']['roles'] = ['global-read']; // set pfy group to global read only role
$config['auth_ldap_groups']['support']['roles'] = ['user']; // set support group as a normal user
```
#### Additional options (usually not needed)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,11 +1,11 @@
{
"/js/app.js": "/js/app.js?id=5ddec7f7302f146a8dcc",
"/js/app.js": "/js/app.js?id=8f48674c187c7d68c740",
"/js/manifest.js": "/js/manifest.js?id=2951ae529be231f05a93",
"/css/vendor.css": "/css/vendor.css?id=2568831af31dbfc3128a",
"/css/app.css": "/css/app.css?id=1cd88608bf4eaee000d8",
"/js/vendor.js": "/js/vendor.js?id=c5fd3d75a63757080dbb",
"/css/app.css": "/css/app.css?id=ddaa1664b2b7b9dc3293",
"/js/vendor.js": "/js/vendor.js?id=9a257386f44b3d7f29bc",
"/js/lang/de.js": "/js/lang/de.js?id=d74df23e729c5dabfee8",
"/js/lang/en.js": "/js/lang/en.js?id=20e52084af3a0a8f4724",
"/js/lang/en.js": "/js/lang/en.js?id=4ea5e56f2ff6fbab1244",
"/js/lang/fr.js": "/js/lang/fr.js?id=22902d30358443ef2877",
"/js/lang/it.js": "/js/lang/it.js?id=6220e138068a7e58387f",
"/js/lang/ru.js": "/js/lang/ru.js?id=f6b7c078755312a0907c",

View File

@ -1,66 +0,0 @@
<?php
use LibreNMS\Authentication\LegacyAuth;
echo '<div style="margin: 10px;">';
if (! Auth::user()->isAdmin()) {
include 'includes/html/error-no-perm.inc.php';
} else {
echo '<h3>Delete User</h3>';
$pagetitle[] = 'Delete user';
if (LegacyAuth::get()->canManageUsers()) {
if ($vars['action'] == 'del') {
$id = (int) $vars['id'];
$user = LegacyAuth::get()->getUser($id);
if ($vars['confirm'] == 'yes') {
if (LegacyAuth::get()->deleteUser($id) >= 0) {
print_message('<div class="infobox">User "' . $user['username'] . '" deleted!');
} else {
print_error('Error deleting user "' . $user['username'] . '"!');
}
} else {
print_error('You have requested deletion of the user "' . $user['username'] . '". This action can not be reversed.<br /><a class="btn btn-danger" href="deluser/action=del/id=' . $id . '/confirm=yes">Click to confirm</a>');
}
}
// FIXME v mysql query should be replaced by authmodule
$userlist = LegacyAuth::get()->getUserlist();
echo '
<form role="form" class="form-horizontal" method="GET" action="">
' . csrf_field() . '
<input type="hidden" name="action" value="del">
<div class="form-group">
<label for="user_id" class="col-sm-2 control-label">Select User: </label>
<div class="col-sm-6">
<select id="user_id" name="id" class="form-control input-sm">
';
foreach ($userlist as $userentry) {
$i++;
echo '<option value="' . $userentry['user_id'] . '">' . $userentry['username'] . '</option>';
}
echo '
</select>
</div>
</div>
<div class="form-group">
<div class="col-sm-2">
</div>
<div class="col-sm-6">
<button class="btn btn-danger btn-sm">Delete User</button>
</div>
</div>
</form>
';
} else {
print_error('Authentication module does not allow user management!');
}//end if
}//end if
echo '</div>';

View File

@ -81,29 +81,33 @@ if (! $auth) {
print_optionbar_end();
$thumb_array = Config::get('graphs.row.normal');
$show_command = isset($vars['showcommand']) && $vars['showcommand'] == 'yes';
if (! $show_command) {
$thumb_array = Config::get('graphs.row.normal');
echo '<table width=100% class="thumbnail_graph_table"><tr>';
echo '<table width=100% class="thumbnail_graph_table"><tr>';
foreach ($thumb_array as $period => $text) {
$graph_array['from'] = Config::get("time.$period");
foreach ($thumb_array as $period => $text) {
$graph_array['from'] = Config::get("time.$period");
$link_array = $vars;
$link_array['from'] = $graph_array['from'];
$link_array['to'] = $graph_array['to'];
$link_array['page'] = 'graphs';
$link = \LibreNMS\Util\Url::generate($link_array);
$link_array = $vars;
$link_array['from'] = $graph_array['from'];
$link_array['to'] = $graph_array['to'];
$link_array['page'] = 'graphs';
$link = \LibreNMS\Util\Url::generate($link_array);
echo '<td style="text-align: center;">';
echo '<b>' . $text . '</b>';
echo '<a href="' . $link . '">';
echo \LibreNMS\Util\Url::lazyGraphTag($graph_array);
echo '</a>';
echo '</td>';
echo '<td style="text-align: center;">';
echo '<b>' . $text . '</b>';
echo '<a href="' . $link . '">';
echo \LibreNMS\Util\Url::lazyGraphTag($graph_array);
echo '</a>';
echo '</td>';
}
echo '</tr></table>';
echo '<hr />';
}
echo '</tr></table>';
$graph_array = $vars;
$graph_array['height'] = Config::get('webui.min_graph_height');
$graph_array['width'] = $graph_width;
@ -124,8 +128,6 @@ if (! $auth) {
}
}
echo '<hr />';
include_once 'includes/html/print-date-selector.inc.php';
echo '<div style="padding-top: 5px";></div>';
@ -148,7 +150,7 @@ if (! $auth) {
// }
echo ' | ';
if (isset($vars['showcommand']) && $vars['showcommand'] == 'yes') {
if ($show_command) {
echo generate_link('Hide RRD Command', $vars, ['page' => 'graphs', 'showcommand' => null]);
} else {
echo generate_link('Show RRD Command', $vars, ['page' => 'graphs', 'showcommand' => 'yes']);
@ -188,7 +190,7 @@ if (! $auth) {
print_optionbar_end();
}
if (! empty($vars['showcommand'])) {
if ($show_command) {
$vars = $graph_array;
$_GET = $graph_array;
$command_only = 1;

View File

@ -30,6 +30,7 @@ return [
'general' => ['name' => 'General Authentication Settings'],
'ad' => ['name' => 'Active Directory Settings'],
'ldap' => ['name' => 'LDAP Settings'],
'radius' => ['name' => 'Radius Settings'],
'socialite' => ['name' => 'Socialite Settings'],
],
'authorization' => [
@ -1259,6 +1260,12 @@ return [
'help' => 'Networks/IPs which will not be discovered automatically. Excludes also IPs from Autodiscovery Networks',
],
],
'radius' => [
'default_roles' => [
'description' => 'Default user roles',
'help' => 'Sets the roles that will be assigned to the user unless Radius sends attributes that specify role(s)',
],
],
'reporting' => [
'error' => [
'description' => 'Send Error Reports',

View File

@ -434,10 +434,15 @@
"group": "auth",
"section": "ad",
"order": 4,
"type": "ldap-groups",
"type": "group-role-map",
"options": {
"groupPlaceholder": "Group Name"
},
"validate": {
"value": "array",
"value.*": "array"
"value.*": "array",
"value.*.roles": "array|required",
"value.*.roles.*": "string"
}
},
"auth_ad_user_filter": {
@ -574,10 +579,15 @@
"group": "auth",
"section": "ldap",
"order": 4,
"type": "ldap-groups",
"type": "group-role-map",
"options": {
"groupPlaceholder": "Group DN"
},
"validate": {
"value": "array",
"value.*": "array"
"value.*": "array",
"value.*.roles": "array|required",
"value.*.roles.*": "string"
}
},
"auth_ldap_port": {
@ -5094,6 +5104,13 @@
"order": 7,
"type": "boolean"
},
"radius.default_roles": {
"default": [],
"group": "auth",
"section": "radius",
"order": 3,
"type": "array"
},
"rancid_configs": {
"default": [],
"type": "array"

View File

@ -64,7 +64,7 @@
},
"required": ["type"],
"additionalProperties": false,
"oneOf": [
"anyOf": [
{
"properties": {
"type": {"const": "select"},
@ -96,6 +96,21 @@
},
"required": ["options", "validate"]
},
{
"properties": {
"type": {"const": "group-role-map"},
"options": {
"type": "object",
"properties": {
"groupPlaceholder": {"type": "string"}
},
"additionalProperties": false
},
"validate": {
"minProperties": 1
}
}
},
{
"properties": {
"type": {
@ -112,7 +127,6 @@
"float",
"graph",
"snmp3auth",
"ldap-groups",
"ad-groups",
"oxidized-maps",
"executable",

View File

@ -1,3 +1,18 @@
abilities:
Columns:
- { Field: id, Type: 'bigint unsigned', 'Null': false, Extra: auto_increment }
- { Field: name, Type: varchar(255), 'Null': false, Extra: '' }
- { Field: title, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: entity_id, Type: 'bigint unsigned', 'Null': true, Extra: '' }
- { Field: entity_type, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: only_owned, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: options, Type: longtext, 'Null': true, Extra: '' }
- { Field: scope, Type: int, 'Null': true, Extra: '' }
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
abilities_scope_index: { Name: abilities_scope_index, Columns: [scope], Unique: false, Type: BTREE }
access_points:
Columns:
- { Field: accesspoint_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
@ -183,6 +198,22 @@ application_metrics:
- { Field: value_prev, Type: double, 'Null': true, Extra: '' }
Indexes:
application_metrics_app_id_metric_unique: { Name: application_metrics_app_id_metric_unique, Columns: [app_id, metric], Unique: true, Type: BTREE }
assigned_roles:
Columns:
- { Field: id, Type: 'bigint unsigned', 'Null': false, Extra: auto_increment }
- { Field: role_id, Type: 'bigint unsigned', 'Null': false, Extra: '' }
- { Field: entity_id, Type: 'bigint unsigned', 'Null': false, Extra: '' }
- { Field: entity_type, Type: varchar(255), 'Null': false, Extra: '' }
- { Field: restricted_to_id, Type: 'bigint unsigned', 'Null': true, Extra: '' }
- { Field: restricted_to_type, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: scope, Type: int, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
assigned_roles_entity_index: { Name: assigned_roles_entity_index, Columns: [entity_id, entity_type, scope], Unique: false, Type: BTREE }
assigned_roles_role_id_index: { Name: assigned_roles_role_id_index, Columns: [role_id], Unique: false, Type: BTREE }
assigned_roles_scope_index: { Name: assigned_roles_scope_index, Columns: [scope], Unique: false, Type: BTREE }
Constraints:
assigned_roles_role_id_foreign: { name: assigned_roles_role_id_foreign, foreign_key: role_id, table: roles, key: id, extra: 'ON DELETE CASCADE ON UPDATE CASCADE' }
authlog:
Columns:
- { Field: id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
@ -1343,6 +1374,21 @@ pdb_ix_peers:
- { Field: timestamp, Type: 'int unsigned', 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [pdb_ix_peers_id], Unique: true, Type: BTREE }
permissions:
Columns:
- { Field: id, Type: 'bigint unsigned', 'Null': false, Extra: auto_increment }
- { Field: ability_id, Type: 'bigint unsigned', 'Null': false, Extra: '' }
- { Field: entity_id, Type: 'bigint unsigned', 'Null': true, Extra: '' }
- { Field: entity_type, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: forbidden, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: scope, Type: int, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
permissions_ability_id_index: { Name: permissions_ability_id_index, Columns: [ability_id], Unique: false, Type: BTREE }
permissions_entity_index: { Name: permissions_entity_index, Columns: [entity_id, entity_type, scope], Unique: false, Type: BTREE }
permissions_scope_index: { Name: permissions_scope_index, Columns: [scope], Unique: false, Type: BTREE }
Constraints:
permissions_ability_id_foreign: { name: permissions_ability_id_foreign, foreign_key: ability_id, table: abilities, key: id, extra: 'ON DELETE CASCADE ON UPDATE CASCADE' }
plugins:
Columns:
- { Field: plugin_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
@ -1767,6 +1813,18 @@ push_subscriptions:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
push_subscriptions_endpoint_unique: { Name: push_subscriptions_endpoint_unique, Columns: [endpoint], Unique: true, Type: BTREE }
push_subscriptions_subscribable_type_subscribable_id_index: { Name: push_subscriptions_subscribable_type_subscribable_id_index, Columns: [subscribable_type, subscribable_id], Unique: false, Type: BTREE }
roles:
Columns:
- { Field: id, Type: 'bigint unsigned', 'Null': false, Extra: auto_increment }
- { Field: name, Type: varchar(255), 'Null': false, Extra: '' }
- { Field: title, Type: varchar(255), 'Null': true, Extra: '' }
- { Field: scope, Type: int, 'Null': true, Extra: '' }
- { Field: created_at, Type: timestamp, 'Null': true, Extra: '' }
- { Field: updated_at, Type: timestamp, 'Null': true, Extra: '' }
Indexes:
PRIMARY: { Name: PRIMARY, Columns: [id], Unique: true, Type: BTREE }
roles_name_unique: { Name: roles_name_unique, Columns: [name, scope], Unique: true, Type: BTREE }
roles_scope_index: { Name: roles_scope_index, Columns: [scope], Unique: false, Type: BTREE }
route:
Columns:
- { Field: route_id, Type: 'int unsigned', 'Null': false, Extra: auto_increment }
@ -2052,7 +2110,6 @@ users:
- { Field: realname, Type: varchar(64), 'Null': false, Extra: '' }
- { Field: email, Type: varchar(64), 'Null': false, Extra: '' }
- { Field: descr, Type: char(30), 'Null': false, Extra: '' }
- { Field: level, Type: tinyint, 'Null': false, Extra: '', Default: '0' }
- { Field: can_modify_passwd, Type: tinyint, 'Null': false, Extra: '', Default: '1' }
- { Field: created_at, Type: timestamp, 'Null': false, Extra: '', Default: '1970-01-02 00:00:01' }
- { Field: updated_at, Type: timestamp, 'Null': false, Extra: '', Default: CURRENT_TIMESTAMP }

View File

@ -1,15 +1,5 @@
parameters:
ignoreErrors:
-
message: "#^If condition is always false\\.$#"
count: 1
path: LibreNMS/Authentication/MysqlAuthorizer.php
-
message: "#^Parameter \\#2 \\$password of method LibreNMS\\\\Authentication\\\\MysqlAuthorizer\\:\\:addUser\\(\\) expects string, null given\\.$#"
count: 1
path: LibreNMS/Authentication/SSOAuthorizer.php
-
message: "#^Property LibreNMS\\\\Data\\\\Store\\\\Rrd\\:\\:\\$async_process \\(LibreNMS\\\\Proc\\) in isset\\(\\) is not nullable\\.$#"
count: 1
@ -205,6 +195,11 @@ parameters:
count: 1
path: app/Http/Controllers/PortGroupController.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:role\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 2
path: app/Http/Controllers/Select/RoleController.php
-
message: "#^Call to an undefined static method App\\\\Models\\\\ServiceTemplate\\:\\:hasAccess\\(\\)\\.$#"
count: 1
@ -235,11 +230,21 @@ parameters:
count: 1
path: app/Http/Kernel.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:role\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 2
path: app/Http/Requests/StoreUserRequest.php
-
message: "#^Cannot access property \\$password on object\\|string\\.$#"
count: 1
path: app/Http/Requests/UpdateUserRequest.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:role\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 2
path: app/Http/Requests/UpdateUserRequest.php
-
message: "#^Access to an undefined property App\\\\Models\\\\DeviceRelatedModel\\:\\:\\$device_id\\.$#"
count: 1
@ -250,6 +255,11 @@ parameters:
count: 1
path: app/Models/UserPref.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:cache\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 1
path: app/Providers/AuthServiceProvider.php
-
message: "#^Result of && is always false\\.$#"
count: 1
@ -270,6 +280,41 @@ parameters:
count: 1
path: app/View/SimpleTemplate.php
-
message: "#^Parameter \\#1 \\$abilities of method Silber\\\\Bouncer\\\\Conductors\\\\GivesAbilities\\:\\:to\\(\\) expects array\\|Illuminate\\\\Database\\\\Eloquent\\\\Model\\|int, string given\\.$#"
count: 1
path: database/factories/UserFactory.php
-
message: "#^Property 'name' does not exist in Silber\\\\Bouncer\\\\Database\\\\Role model\\.$#"
count: 1
path: database/factories/UserFactory.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:role\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 1
path: database/factories/UserFactory.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:refresh\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 2
path: database/migrations/2023_06_18_201914_migrate_level_to_roles.php
-
message: "#^Parameter \\#1 \\$abilities of method Silber\\\\Bouncer\\\\Conductors\\\\GivesAbilities\\:\\:to\\(\\) expects array\\|Illuminate\\\\Database\\\\Eloquent\\\\Model\\|int, string given\\.$#"
count: 1
path: database/seeders/RolesSeeder.php
-
message: "#^Property 'name' does not exist in Silber\\\\Bouncer\\\\Database\\\\Role model\\.$#"
count: 2
path: database/seeders/RolesSeeder.php
-
message: "#^Static method Silber\\\\Bouncer\\\\BouncerFacade\\:\\:role\\(\\) invoked with 0 parameters, 1 required\\.$#"
count: 2
path: database/seeders/RolesSeeder.php
-
message: "#^Parameter \\#4 \\$transport of function addHost expects string, int given\\.$#"
count: 1

View File

@ -0,0 +1,145 @@
<!--
- LibrenmsSelect.vue
-
- 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>
-->
<template>
<select :multiple="multiple"></select>
</template>
<script>
export default {
name: "LibrenmsSelect",
props: {
routeName: {
type: String,
required: true
},
placeholder: {
type: String,
default: '',
},
allowClear: {
type: Boolean,
default: true
},
multiple: {
type: Boolean,
default: false
},
value: {
type: [String, Number, Array],
default: ''
}
},
model: {
event: 'change',
prop: 'value'
},
data: () => ({
select2: null
}),
methods: {
checkValue() {
if (this.value === '' || this.value === []) {
return true;
}
// search for missing options and fetch them
let values = this.value instanceof Array ? this.value : [this.value];
if (this.select2.find("option").filter((id, el) => values.includes(el.value)).length < values.length) {
axios.get(route(this.routeName), {params: {id: values.join(',')}}).then((response) => {
response.data.results.forEach((item) => {
if (values.find(x => x == item.id) !== undefined) {
this.select2.append(new Option(item.text, item.id, false, true));
}
})
this.select2.trigger('change');
});
return false;
}
return true;
}
},
watch: {
value(value) {
if (value instanceof Object && value.hasOwnProperty('id') && value.hasOwnProperty('text')) {
this.select2.append(new Option(value.text, value.id, true, true))
.trigger('change');
return;
}
// check value and if the value doesn't exist, cancel this update to fetch it
if (! this.checkValue()) {
return;
}
if (value instanceof Array) {
this.select2.val([...value]);
} else {
this.select2.val([value]);
}
this.select2.trigger('change');
}
},
computed: {
settings() {
return {
theme: "bootstrap",
dropdownAutoWidth : true,
width: "auto",
allowClear: Boolean(this.allowClear),
placeholder: this.placeholder,
multiple: this.multiple,
ajax: {
url: route(this.routeName).toString(),
delay: 250,
cache: true
}
}
}
},
mounted() {
this.select2 = $(this.$el);
this.checkValue();
this.select2.select2(this.settings)
.on('select2:select select2:unselect', ev => {
this.$emit('change', this.select2.val());
this.$emit('select', ev['params']['data']);
});
},
beforeDestroy() {
this.select2.select2('destroy');
}
}
</script>
<style scoped>
</style>

View File

@ -23,8 +23,8 @@
-->
<template>
<div :class="['form-group', 'has-feedback', setting.class, feedback]">
<label :for="setting.name" class="col-sm-5 control-label" v-tooltip="{ content: setting.name }">
<div :class="['form-group', 'row', 'has-feedback', setting.class, feedback]">
<label :for="setting.name" class="col-sm-5 col-md-3 col-form-label" v-tooltip="{ content: setting.name }">
{{ getDescription() }}
<span v-if="setting.units">({{ getUnits() }})</span>
</label>
@ -42,7 +42,7 @@
></component>
<span class="form-control-feedback"></span>
</div>
<div class="col-sm-2">
<div>
<button :style="{'opacity': showResetToDefault()?1:0}" @click="resetToDefault" class="btn btn-default" :class="{'disable-events': ! showResetToDefault()}" type="button" v-tooltip="{ content: $t('Reset to default') }"><i class="fa fa-refresh"></i></button>
<button :style="{'opacity': showUndo()?1:0}" @click="resetToInitial" class="btn btn-primary" :class="{'disable-events': ! showUndo()}" type="button" v-tooltip="{ content: $t('Undo') }"><i class="fa fa-undo"></i></button>
<div v-if="hasHelp()" v-tooltip="{content: getHelp(), trigger: 'hover click'}" class="fa fa-fw fa-lg fa-question-circle"></div>

View File

@ -0,0 +1,123 @@
<!--
- SettingGroupRoleMap.vue
-
- 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/>.
-
- @package LibreNMS
- @link https://www.librenms.org
- @copyright 2023 Tony Murray
- @author Tony Murray <murraytony@gmail.com>
-->
<template>
<div v-tooltip="disabled ? $t('settings.readonly') : false">
<div v-for="(data, group) in localList" class="tw-flex">
<input type="text"
class="form-control !tw-w-auto"
:value="group"
:readonly="disabled"
:placeholder="options.groupPlaceholder"
@blur="updateItem(group, $event.target.value)"
@keyup.enter="updateItem(group, $event.target.value)"
>
<librenms-select class="form-control tw-flex-grow" @change="updateRoles(group, $event)" route-name="ajax.select.role" :value="data.roles" multiple :disabled="disabled" :allow-clear="false"></librenms-select>
<button v-if="!disabled" @click="removeItem(group)" type="button" class="btn btn-danger"><i class="fa fa-minus-circle"></i></button>
</div>
<div v-if="!disabled" class="tw-flex">
<input type="text" class="form-control !tw-w-auto" v-model="newItem" :placeholder="options.groupPlaceholder">
<librenms-select class="form-control tw-flex-grow" v-model="newItemRoles" route-name="ajax.select.role" placeholder="Role" multiple :disabled="disabled" :allow-clear="false"></librenms-select>
<button @click="addItem" type="button" class="btn btn-primary"><i class="fa fa-plus-circle"></i></button>
</div>
</div>
</template>
<script>
import BaseSetting from "./BaseSetting";
import LibrenmsSelect from "./LibrenmsSelect.vue";
export default {
name: "SettingGroupRoleMap",
components: {LibrenmsSelect},
mixins: [BaseSetting],
data() {
return {
newItem: "",
newItemRoles: [],
localList: this.parseValue(this.value),
}
},
methods: {
addItem() {
this.localList[this.newItem] = {roles: this.newItemRoles};
this.newItem = "";
this.newItemRoles = [];
this.$emit('input', this.localList)
},
removeItem(index) {
delete this.localList[index]
this.$emit('input', this.localList)
},
updateItem(oldValue, newValue) {
this.localList = Object.keys(this.localList).reduce((newList, current) => {
let key = (current === oldValue ? newValue : current);
newList[key] = this.localList[current];
return newList;
}, {});
this.$emit('input', this.localList)
},
updateRoles(group, roles) {
console.log(group, roles, this.lock);
this.localList[group].roles = roles;
this.$emit('input', this.localList)
},
parseValue(value) {
// empty lists parse to an array
if (Array.isArray(value)) {
return {};
}
const levels = {
1: "user",
5: "global-read",
10: "admin",
};
for (const group of Object.keys(value)) {
if (! value[group].hasOwnProperty('roles') && value[group].hasOwnProperty('level')) {
value[group].roles = levels[value[group].level] ? [levels[value[group].level]] : [];
delete value[group]["level"];
}
}
return value;
}
},
watch: {
value() {
this.localList = this.parseValue(this.value);
}
}
}
</script>
<style scoped>
div >>> .select2-container {
flex-grow: 1;
}
div >>> .select2-selection--multiple .select2-search--inline .select2-search__field {
width: 0.75em !important;
}
</style>

View File

@ -24,70 +24,27 @@
<template>
<div>
<select class="form-control"
:name="name"
<librenms-select class="form-control"
:value="value"
:route-name="'ajax.select.' + this.options.target"
:placeholder="this.options.placeholder"
:allow-clear="this.options.allowClear"
:required="required"
:disabled="disabled"
@change="$emit('change', $event)"
>
</select>
</librenms-select>
</div>
</template>
<script>
import BaseSetting from "./BaseSetting";
import LibrenmsSelect from "./LibrenmsSelect.vue";
export default {
name: "SettingSelectDynamic",
mixins: [BaseSetting],
data() {
return {
select2: null
};
},
watch: {
value(value) {
this.select2.val(value).trigger('change');
}
},
computed: {
settings() {
return {
theme: "bootstrap",
dropdownAutoWidth : true,
width: "auto",
allowClear: Boolean(this.options.allowClear),
placeholder: this.options.placeholder,
ajax: {
url: route('ajax.select.' + this.options.target).toString(),
delay: 250,
data: this.options.callback
}
}
}
},
mounted() {
// load initial data
axios.get(route('ajax.select.' + this.options.target), {params: {id: this.value}}).then((response) => {
response.data.results.forEach((item) => {
if (item.id == this.value) {
this.select2.append(new Option(item.text, item.id, true, true))
.trigger('change');
}
})
});
this.select2 = $(this.$el)
.find('select')
.select2(this.settings)
.on('select2:select select2:unselect', ev => {
this.$emit('change', this.select2.val());
this.$emit('select', ev['params']['data']);
});
},
beforeDestroy() {
this.select2.select2('destroy');
}
components: {LibrenmsSelect},
mixins: [BaseSetting]
}
</script>

View File

@ -3,7 +3,7 @@
@section('title', __('settings.title'))
@section('content')
<div class="container">
<div class="container-fluid">
<div id="app">
<librenms-settings
prefix="{{ url('settings') }}"

View File

@ -30,17 +30,16 @@
</div>
</div>
@can('admin')
<div class="form-group @if($errors->has('level')) has-error @endif">
<label for="level" class="control-label col-sm-3">{{ __('Level') }}</label>
@can('viewAny', Bouncer::role())
<div class="form-group @if($errors->has('roles')) has-error @endif">
<label for="level" class="control-label col-sm-3">{{ __('Roles') }}</label>
<div class="col-sm-9">
<select class="form-control" id="level" name="level">
<option value="1">{{ __('Normal') }}</option>
<option value="5" @if(old('level', $user->level) == 5) selected @endif>{{ __('Global Read') }}</option>
<option value="10" @if(old('level', $user->level) == 10) selected @endif>{{ __('Admin') }}</option>
@if(old('level', $user->level) == 11)<option value="11" selected>{{ __('Demo') }}</option>@endif
<select class="form-control" id="roles" name="roles[]" multiple @cannot('manage', Bouncer::role()) readonly @endcannot>
@foreach(Bouncer::role()->all() as $role)
<option value="{{ $role->name }}" @if(collect(old('roles', $user->roles->pluck('name')))->contains($role->name)) selected @endif>{{ __($role->title) }}</option>
@endforeach
</select>
<span class="help-block">{{ $errors->first('level') }}</span>
<span class="help-block">{{ $errors->first('roles') }}</span>
</div>
</div>
@endcan

View File

@ -16,9 +16,9 @@
<th data-column-id="user_id" data-visible="false" data-identifier="true" data-type="numeric">{{ __('ID') }}</th>
<th data-column-id="username" data-formatter="text">{{ __('Username') }}</th>
<th data-column-id="realname" data-formatter="text">{{ __('Real Name') }}</th>
<th data-column-id="level" data-formatter="level" data-type="numeric">{{ __('Access') }}</th>
<th data-column-id="roles" data-formatter="roles">{{ __('Roles') }}</th>
<th data-column-id="auth_type" data-visible="{{ $multiauth ? 'true' : 'false' }}">{{ __('auth.title') }}</th>
<th data-column-id="email">{{ __('Email') }}</th>
<th data-column-id="email" data-formatter="text">{{ __('Email') }}</th>
<th data-column-id="timezone">{{ __('Timezone') }}</th>
@if(\LibreNMS\Authentication\LegacyAuth::getType() == 'mysql')
<th data-column-id="enabled" data-formatter="enabled">{{ __('Enabled') }}</th>
@ -36,7 +36,7 @@
<td>{{ $user->user_id }}</td>
<td>{{ $user->username }}</td>
<td>{{ $user->realname }}</td>
<td>{{ $user->level }}</td>
<td>{{ $user->roles->pluck('title') }}</td>
<td>{{ $user->auth_type }}</td>
<td>{{ $user->email }}</td>
<td>{{ \App\Models\UserPref::getPref($user, 'timezone') ?: "Browser Timezone" }}</td>
@ -93,12 +93,8 @@
var delete_button = '<button type="button" title="{{ __('Delete') }}" class="btn btn-sm btn-danger" onclick="return delete_user(' + row['user_id'] + ', \'' + row['username'] + '\');">' +
'<i class="fa fa-trash"></i></button> ';
// FIXME don't show for super admin
var manage_button = '<form action="{{ url('edituser') }}/" method="GET"';
if (row['level'] >= 5) {
manage_button += ' style="visibility:hidden;"'
}
manage_button += '>@csrf<input type="hidden" name="user_id" value="' + row['user_id'] +
'"><button type="submit" title="{{ __('Manage Access') }}" class="btn btn-sm btn-primary"><i class="fa fa-tasks"></i></button>' +
'</form> ';
@ -110,24 +106,25 @@
return output
},
level: function (column, row) {
var level = row[column.id];
if (level == 10) {
return '{{ __('Admin') }}';
} else if (level == 5) {
return '{{ __('Global Read') }}';
} else if (level == 11) {
return '{{ __('Demo') }}';
}
roles: function (column, row) {
let roles = JSON.parse(row[column.id]);
let div = document.createElement('div');
return '{{ __('Normal') }}';
roles.forEach((role) => {
let label = document.createElement('span');
label.className = 'label label-info tw-mr-1';
label.innerText = role;
div.appendChild(label);
})
return div.outerHTML;
}
}
});
@if(\LibreNMS\Config::get('auth_mechanism') == 'mysql')
@can('create', \App\Models\User::class)
$('.actionBar').append('<div class="pull-left"><a href="{{ route('users.create') }}" type="button" class="btn btn-primary">{{ __('Add User') }}</a></div>');
@endif
@endcan
user_grid.css('display', 'table'); // done loading, show
});

View File

@ -186,6 +186,14 @@
</x-panel>
@endconfig
<x-panel title="{{ __('Roles') }}">
@forelse(auth()->user()->roles->pluck('title') as $role)
<span class="label label-info tw-mr-1">{{ $role }}</span>
@empty
<strong class="red">{{ __('No roles!') }}</strong>
@endforelse
</x-panel>
<x-panel title="{{ __('Device Permissions') }}">
@if(auth()->user()->hasGlobalAdmin())
<strong class="blue">{{ __('Global Administrative Access') }}</strong>

View File

@ -164,6 +164,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('device-field', 'DeviceFieldController')->name('ajax.select.device-field');
Route::get('device-group', 'DeviceGroupController')->name('ajax.select.device-group');
Route::get('port-group', 'PortGroupController')->name('ajax.select.port-group');
Route::get('role', 'RoleController')->name('ajax.select.role');
Route::get('eventlog', 'EventlogController')->name('ajax.select.eventlog');
Route::get('graph', 'GraphController')->name('ajax.select.graph');
Route::get('graph-aggregate', 'GraphAggregateController')->name('ajax.select.graph-aggregate');
@ -172,6 +173,7 @@ Route::middleware(['auth'])->group(function () {
Route::get('syslog', 'SyslogController')->name('ajax.select.syslog');
Route::get('location', 'LocationController')->name('ajax.select.location');
Route::get('munin', 'MuninPluginController')->name('ajax.select.munin');
Route::get('role', 'RoleController')->name('ajax.select.role');
Route::get('service', 'ServiceController')->name('ajax.select.service');
Route::get('template', 'ServiceTemplateController')->name('ajax.select.template');
Route::get('poller-group', 'PollerGroupController')->name('ajax.select.poller-group');

View File

@ -9,8 +9,7 @@ use LibreNMS\Util\Debug;
$options = getopt('u:rldvh');
if (isset($options['h']) || (! isset($options['l']) && ! isset($options['u']))) {
echo ' -u <username> (Required) username to test
-l List all users (checks that auth can enumerate all allowed users)
-d Enable debug output
] -d Enable debug output
-v Enable verbose debug output
-h Display this help message
';
@ -88,17 +87,6 @@ try {
}
}
if (isset($options['l'])) {
$users = $authorizer->getUserlist();
$output = array_map(function ($user) {
return "{$user['username']} ({$user['user_id']})";
}, $users);
echo 'Users: ' . implode(', ', $output) . PHP_EOL;
echo 'Total users: ' . count($users) . PHP_EOL;
exit;
}
$test_username = $options['u'];
$auth = false;

View File

@ -25,6 +25,7 @@
namespace LibreNMS\Tests;
use App\Models\User;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Str;
use LibreNMS\Authentication\LegacyAuth;
@ -118,8 +119,7 @@ class AuthSSOTest extends DBTestCase
$this->assertTrue($a->authenticate(['username' => $user]));
// Retrieve it and validate
$dbuser = $a->getUser($a->getUserid($user));
$this->assertFalse($dbuser);
$this->assertFalse(User::thisAuth()->where('username', $user)->exists());
}
// Excercise general auth flow with creation enabled
@ -138,10 +138,10 @@ class AuthSSOTest extends DBTestCase
$this->assertTrue($a->authenticate(['username' => $user]));
// Retrieve it and validate
$dbuser = $a->getUser($a->getUserid($user));
$this->assertSame($dbuser['realname'], $a->authSSOGetAttr(Config::get('sso.realname_attr')));
$this->assertTrue($dbuser['level'] == -1);
$this->assertSame($dbuser['email'], $a->authSSOGetAttr(Config::get('sso.email_attr')));
$dbuser = User::thisAuth()->where('username', $user)->firstOrNew();
$this->assertSame($dbuser->realname, $a->authSSOGetAttr(Config::get('sso.realname_attr')));
$this->assertEmpty($dbuser->getRoles());
$this->assertSame($dbuser->email, $a->authSSOGetAttr(Config::get('sso.email_attr')));
// Change a few things and reauth
$_SERVER['mail'] = 'test@example.net';
@ -150,10 +150,10 @@ class AuthSSOTest extends DBTestCase
$this->assertTrue($a->authenticate(['username' => $user]));
// Retrieve it and validate the update was not persisted
$dbuser = $a->getUser($a->getUserid($user));
$this->assertFalse($a->authSSOGetAttr(Config::get('sso.realname_attr')) === $dbuser['realname']);
$this->assertFalse($dbuser['level'] === '10');
$this->assertFalse($a->authSSOGetAttr(Config::get('sso.email_attr')) === $dbuser['email']);
$dbuser = User::thisAuth()->where('username', $user)->firstOrNew();
$this->assertFalse($a->authSSOGetAttr(Config::get('sso.realname_attr')) === $dbuser->realname);
$this->assertFalse($dbuser->roles()->where('name', 'admin')->exists());
$this->assertFalse($a->authSSOGetAttr(Config::get('sso.email_attr')) === $dbuser->email);
}
// Excercise general auth flow with updates enabled
@ -166,19 +166,19 @@ class AuthSSOTest extends DBTestCase
// Create a random username and store it with the defaults
$this->basicEnvironmentEnv();
$user = $this->makeUser();
$this->assertTrue($a->authenticate(['username' => $user]));
$this->assertTrue(auth()->attempt(['username' => $user]));
// Change a few things and reauth
$_SERVER['mail'] = 'test@example.net';
$_SERVER['displayName'] = 'Testier User';
Config::set('sso.static_level', 10);
$this->assertTrue($a->authenticate(['username' => $user]));
$this->assertTrue(auth()->attempt(['username' => $user]));
// Retrieve it and validate the update persisted
$dbuser = $a->getUser($a->getUserid($user));
$this->assertSame($dbuser['realname'], $a->authSSOGetAttr(Config::get('sso.realname_attr')));
$this->assertTrue($dbuser['level'] == 10);
$this->assertSame($dbuser['email'], $a->authSSOGetAttr(Config::get('sso.email_attr')));
$dbuser = User::thisAuth()->where('username', $user)->firstOrNew();
$this->assertSame($dbuser->realname, $a->authSSOGetAttr(Config::get('sso.realname_attr')));
$this->assertTrue($dbuser->roles()->where('name', 'admin')->exists());
$this->assertSame($dbuser->email, $a->authSSOGetAttr(Config::get('sso.email_attr')));
}
// Check some invalid authentication modes
@ -212,13 +212,13 @@ class AuthSSOTest extends DBTestCase
unset($_SERVER['displayName']);
unset($_SERVER['mail']);
$this->assertTrue($a->authenticate(['username' => $this->makeUser()]));
$this->assertTrue(auth()->attempt(['username' => $this->makeUser()]));
$this->basicEnvironmentHeader();
unset($_SERVER['HTTP_DISPLAYNAME']);
unset($_SERVER['HTTP_MAIL']);
$this->assertTrue($a->authenticate(['username' => $this->makeUser()]));
$this->assertTrue(auth()->attempt(['username' => $this->makeUser()]));
}
// Document the modules current behaviour, so that changes trigger test failures
@ -347,7 +347,7 @@ class AuthSSOTest extends DBTestCase
public function testLevelCaulculationFromAttr(): void
{
/** @var \LibreNMS\Authentication\SSOAuthorizer */
/** @var \LibreNMS\Authentication\SSOAuthorizer $a */
$a = LegacyAuth::reset();
Config::set('sso.mode', 'env');
@ -355,37 +355,42 @@ class AuthSSOTest extends DBTestCase
//Integer
Config::set('sso.level_attr', 'level');
$_SERVER['level'] = 9;
$this->assertSame(9, $a->authSSOCalculateLevel());
$_SERVER['level'] = 5;
$this->assertSame(['global-read'], $a->getRoles(''));
//String
Config::set('sso.level_attr', 'level');
$_SERVER['level'] = '9';
$this->assertSame(9, $a->authSSOCalculateLevel());
$_SERVER['level'] = '5';
$this->assertSame(['global-read'], $a->getRoles(''));
// invalid level
Config::set('sso.level_attr', 'level');
$_SERVER['level'] = 9;
$this->assertSame([], $a->getRoles(''));
//Invalid String
Config::set('sso.level_attr', 'level');
$_SERVER['level'] = 'foobar';
$this->expectException('LibreNMS\Exceptions\AuthenticationException');
$a->authSSOCalculateLevel();
$a->getRoles('');
//null
Config::set('sso.level_attr', 'level');
$_SERVER['level'] = null;
$this->expectException('LibreNMS\Exceptions\AuthenticationException');
$a->authSSOCalculateLevel();
$a->getRoles('');
//Unset pointer
Config::forget('sso.level_attr');
$_SERVER['level'] = '9';
$this->expectException('LibreNMS\Exceptions\AuthenticationException');
$a->authSSOCalculateLevel();
$a->getRoles('');
//Unset attr
Config::set('sso.level_attr', 'level');
unset($_SERVER['level']);
$this->expectException('LibreNMS\Exceptions\AuthenticationException');
$a->authSSOCalculateLevel();
$a->getRoles('');
}
public function testGroupParsing(): void

View File

@ -36,6 +36,7 @@ class BasicApiTest extends DBTestCase
public function testListDevices(): void
{
/** @var User $user */
$user = User::factory()->admin()->create();
$token = ApiToken::generateToken($user);
$device = Device::factory()->create();

View File

@ -42,8 +42,8 @@ class TestConfigCommands extends InMemoryDbTestCase
// set inside
$this->assertCliGets('auth_ldap_groups.somegroup', null);
$this->artisan('config:set', ['setting' => 'auth_ldap_groups.somegroup', 'value' => '{"level": 3}'])->assertExitCode(0);
$this->assertCliGets('auth_ldap_groups.somegroup', ['level' => 3]);
$this->artisan('config:set', ['setting' => 'auth_ldap_groups.somegroup', 'value' => '{"roles": ["banana"]}'])->assertExitCode(0);
$this->assertCliGets('auth_ldap_groups.somegroup', ['roles' => ['banana']]);
$this->artisan('config:set', ['setting' => 'auth_ldap_groups.somegroup'])
->expectsConfirmation(trans('commands.config:set.forget_from', ['path' => 'somegroup', 'parent' => 'auth_ldap_groups']), 'yes')
->assertExitCode(0);