diff --git a/LibreNMS/Alert/AlertUtil.php b/LibreNMS/Alert/AlertUtil.php
index dadf54ef51..393c1afd19 100644
--- a/LibreNMS/Alert/AlertUtil.php
+++ b/LibreNMS/Alert/AlertUtil.php
@@ -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;
}
}
diff --git a/LibreNMS/Authentication/ADAuthorizationAuthorizer.php b/LibreNMS/Authentication/ADAuthorizationAuthorizer.php
index 29e2b41d08..a4bad740c4 100644
--- a/LibreNMS/Authentication/ADAuthorizationAuthorizer.php
+++ b/LibreNMS/Authentication/ADAuthorizationAuthorizer.php
@@ -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)
diff --git a/LibreNMS/Authentication/ActiveDirectoryAuthorizer.php b/LibreNMS/Authentication/ActiveDirectoryAuthorizer.php
index e209a98a6f..77036cf0e7 100644
--- a/LibreNMS/Authentication/ActiveDirectoryAuthorizer.php
+++ b/LibreNMS/Authentication/ActiveDirectoryAuthorizer.php
@@ -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)
diff --git a/LibreNMS/Authentication/ActiveDirectoryCommon.php b/LibreNMS/Authentication/ActiveDirectoryCommon.php
index 8cf262a122..8c7b04abe4 100644
--- a/LibreNMS/Authentication/ActiveDirectoryCommon.php
+++ b/LibreNMS/Authentication/ActiveDirectoryCommon.php
@@ -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,
];
}
diff --git a/LibreNMS/Authentication/AuthorizerBase.php b/LibreNMS/Authentication/AuthorizerBase.php
index 7a3a1cc4de..f29661a2d9 100644
--- a/LibreNMS/Authentication/AuthorizerBase.php
+++ b/LibreNMS/Authentication/AuthorizerBase.php
@@ -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
+ }
}
diff --git a/LibreNMS/Authentication/HttpAuthAuthorizer.php b/LibreNMS/Authentication/HttpAuthAuthorizer.php
index 215e61317d..fbfc6cbb29 100644
--- a/LibreNMS/Authentication/HttpAuthAuthorizer.php
+++ b/LibreNMS/Authentication/HttpAuthAuthorizer.php
@@ -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);
diff --git a/LibreNMS/Authentication/LdapAuthorizationAuthorizer.php b/LibreNMS/Authentication/LdapAuthorizationAuthorizer.php
index fe00b811b2..cd3f3ad7a3 100644
--- a/LibreNMS/Authentication/LdapAuthorizationAuthorizer.php
+++ b/LibreNMS/Authentication/LdapAuthorizationAuthorizer.php
@@ -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;
}
diff --git a/LibreNMS/Authentication/LdapAuthorizer.php b/LibreNMS/Authentication/LdapAuthorizer.php
index 037a7382b6..3d4522484b 100644
--- a/LibreNMS/Authentication/LdapAuthorizer.php
+++ b/LibreNMS/Authentication/LdapAuthorizer.php
@@ -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]),
];
}
diff --git a/LibreNMS/Authentication/MysqlAuthorizer.php b/LibreNMS/Authentication/MysqlAuthorizer.php
index 2358146fdb..b774144010 100644
--- a/LibreNMS/Authentication/MysqlAuthorizer.php
+++ b/LibreNMS/Authentication/MysqlAuthorizer.php
@@ -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();
- }
}
diff --git a/LibreNMS/Authentication/RadiusAuthorizer.php b/LibreNMS/Authentication/RadiusAuthorizer.php
index bdd3ff42e7..cbca8cd8c9 100644
--- a/LibreNMS/Authentication/RadiusAuthorizer.php
+++ b/LibreNMS/Authentication/RadiusAuthorizer.php
@@ -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());
+ }
}
diff --git a/LibreNMS/Authentication/SSOAuthorizer.php b/LibreNMS/Authentication/SSOAuthorizer.php
index eac3394756..fc4241ff3f 100644
--- a/LibreNMS/Authentication/SSOAuthorizer.php
+++ b/LibreNMS/Authentication/SSOAuthorizer.php
@@ -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');
}
diff --git a/LibreNMS/Enum/LegacyAuthLevel.php b/LibreNMS/Enum/LegacyAuthLevel.php
new file mode 100644
index 0000000000..a8a1cf6636
--- /dev/null
+++ b/LibreNMS/Enum/LegacyAuthLevel.php
@@ -0,0 +1,31 @@
+ 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;
+ }
+}
diff --git a/LibreNMS/IRCBot.php b/LibreNMS/IRCBot.php
index 39357c3757..555299a83a 100644
--- a/LibreNMS/IRCBot.php
+++ b/LibreNMS/IRCBot.php
@@ -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;
diff --git a/LibreNMS/Interfaces/Authentication/Authorizer.php b/LibreNMS/Interfaces/Authentication/Authorizer.php
index a3ba4dcfc4..dd5db5c2eb 100644
--- a/LibreNMS/Interfaces/Authentication/Authorizer.php
+++ b/LibreNMS/Interfaces/Authentication/Authorizer.php
@@ -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;
}
diff --git a/app/Console/Commands/AddUserCommand.php b/app/Console/Commands/AddUserCommand.php
index 847ba49b63..c9101b9870 100644
--- a/app/Console/Commands/AddUserCommand.php
+++ b/app/Console/Commands/AddUserCommand.php
@@ -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();
diff --git a/app/Http/Controllers/Install/MakeUserController.php b/app/Http/Controllers/Install/MakeUserController.php
index c7b40fe8e8..a162feb556 100644
--- a/app/Http/Controllers/Install/MakeUserController.php
+++ b/app/Http/Controllers/Install/MakeUserController.php
@@ -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();
diff --git a/app/Http/Controllers/Select/RoleController.php b/app/Http/Controllers/Select/RoleController.php
new file mode 100644
index 0000000000..626cb5a2d8
--- /dev/null
+++ b/app/Http/Controllers/Select/RoleController.php
@@ -0,0 +1,46 @@
+.
+ *
+ * @package LibreNMS
+ * @link http://librenms.org
+ * @copyright 2023 Tony Murray
+ * @author Tony Murray
"+this.$t(this.prefix+".readonly")),t},hasHelp:function(){var t=this.prefix+".settings."+this.setting.name+".help";return this.$te(t)||this.$te(t,this.$i18n.fallbackLocale)},resetToDefault:function(){var t=this;axios.delete(route(this.prefix+".destroy",this.getRouteParams())).then((function(e){t.value=e.data.value,t.feedback="has-success",setTimeout((function(){return t.feedback=""}),3e3)})).catch((function(e){t.feedback="has-error",setTimeout((function(){return t.feedback=""}),3e3),toastr.error(e.response.data.message)}))},resetToInitial:function(){this.changeValue(this.setting.value)},showResetToDefault:function(){return!this.setting.overridden&&!_.isEqual(this.value,this.setting.default)},showUndo:function(){return!_.isEqual(this.setting.value,this.value)},getRouteParams:function(){var t=[this.setting.name];return this.id&&t.unshift(this.id),t},getComponent:function(){var t="Setting"+this.setting.type.toString().replace(/(-[a-z]|^[a-z])/g,(function(t){return t.toUpperCase().replace("-","")}));return void 0!==Vue.options.components[t]?t:"SettingNull"}}},s=n;var i=a(3379),o=a.n(i),r=a(7612),l={insert:"head",singleton:!1};o()(r.Z,l);r.Z.locals;const u=(0,a(1900).Z)(s,(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{class:["form-group","has-feedback",t.setting.class,t.feedback]},[a("label",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.setting.name},expression:"{ content: setting.name }"}],staticClass:"col-sm-5 control-label",attrs:{for:t.setting.name}},[t._v("\n "+t._s(t.getDescription())+"\n "),t.setting.units?a("span",[t._v("("+t._s(t.getUnits())+")")]):t._e()]),t._v(" "),a("div",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:!!t.setting.disabled&&t.$t(this.prefix+".readonly")},expression:"{ content: setting.disabled ? $t(this.prefix + '.readonly') : false }"}],staticClass:"col-sm-5"},[a(t.getComponent(),{tag:"component",attrs:{value:t.value,name:t.setting.name,pattern:t.setting.pattern,disabled:t.setting.overridden,required:t.setting.required,options:t.setting.options,"update-status":t.updateStatus},on:{input:function(e){return t.changeValue(e)},change:function(e){return t.changeValue(e)}}}),t._v(" "),a("span",{staticClass:"form-control-feedback"})],1),t._v(" "),a("div",{staticClass:"col-sm-2"},[a("button",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.$t("Reset to default")},expression:"{ content: $t('Reset to default') }"}],staticClass:"btn btn-default",class:{"disable-events":!t.showResetToDefault()},style:{opacity:t.showResetToDefault()?1:0},attrs:{type:"button"},on:{click:t.resetToDefault}},[a("i",{staticClass:"fa fa-refresh"})]),t._v(" "),a("button",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.$t("Undo")},expression:"{ content: $t('Undo') }"}],staticClass:"btn btn-primary",class:{"disable-events":!t.showUndo()},style:{opacity:t.showUndo()?1:0},attrs:{type:"button"},on:{click:t.resetToInitial}},[a("i",{staticClass:"fa fa-undo"})]),t._v(" "),t.hasHelp()?a("div",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.getHelp(),trigger:"hover click"},expression:"{content: getHelp(), trigger: 'hover click'}"}],staticClass:"fa fa-fw fa-lg fa-question-circle"}):t._e()])])}),[],!1,null,"d23a875a",null).exports},2872:(t,e,a)=>{"use strict";a.r(e),a.d(e,{default:()=>c});function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var a=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==a)return;var n,s,i=[],o=!0,r=!1;try{for(a=a.call(t);!(o=(n=a.next()).done)&&(i.push(n.value),!e||i.length!==e);o=!0);}catch(t){r=!0,s=t}finally{try{o||null==a.return||a.return()}finally{if(r)throw s}}return i}(t,e)||function(t,e){if(!t)return;if("string"==typeof t)return s(t,e);var a=Object.prototype.toString.call(t).slice(8,-1);"Object"===a&&t.constructor&&(a=t.constructor.name);if("Map"===a||"Set"===a)return Array.from(t);if("Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a))return s(t,e)}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t,e){(null==e||e>t.length)&&(e=t.length);for(var a=0,n=new Array(e);a
"+this.$t(this.prefix+".readonly")),t},hasHelp:function(){var t=this.prefix+".settings."+this.setting.name+".help";return this.$te(t)||this.$te(t,this.$i18n.fallbackLocale)},resetToDefault:function(){var t=this;axios.delete(route(this.prefix+".destroy",this.getRouteParams())).then((function(e){t.value=e.data.value,t.feedback="has-success",setTimeout((function(){return t.feedback=""}),3e3)})).catch((function(e){t.feedback="has-error",setTimeout((function(){return t.feedback=""}),3e3),toastr.error(e.response.data.message)}))},resetToInitial:function(){this.changeValue(this.setting.value)},showResetToDefault:function(){return!this.setting.overridden&&!_.isEqual(this.value,this.setting.default)},showUndo:function(){return!_.isEqual(this.setting.value,this.value)},getRouteParams:function(){var t=[this.setting.name];return this.id&&t.unshift(this.id),t},getComponent:function(){var t="Setting"+this.setting.type.toString().replace(/(-[a-z]|^[a-z])/g,(function(t){return t.toUpperCase().replace("-","")}));return void 0!==Vue.options.components[t]?t:"SettingNull"}}},s=n;var i=a(3379),r=a.n(i),o=a(1111),l={insert:"head",singleton:!1};r()(o.Z,l);o.Z.locals;const u=(0,a(1900).Z)(s,(function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("div",{class:["form-group","row","has-feedback",t.setting.class,t.feedback]},[a("label",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.setting.name},expression:"{ content: setting.name }"}],staticClass:"col-sm-5 col-md-3 col-form-label",attrs:{for:t.setting.name}},[t._v("\n "+t._s(t.getDescription())+"\n "),t.setting.units?a("span",[t._v("("+t._s(t.getUnits())+")")]):t._e()]),t._v(" "),a("div",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:!!t.setting.disabled&&t.$t(this.prefix+".readonly")},expression:"{ content: setting.disabled ? $t(this.prefix + '.readonly') : false }"}],staticClass:"col-sm-5"},[a(t.getComponent(),{tag:"component",attrs:{value:t.value,name:t.setting.name,pattern:t.setting.pattern,disabled:t.setting.overridden,required:t.setting.required,options:t.setting.options,"update-status":t.updateStatus},on:{input:function(e){return t.changeValue(e)},change:function(e){return t.changeValue(e)}}}),t._v(" "),a("span",{staticClass:"form-control-feedback"})],1),t._v(" "),a("div",[a("button",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.$t("Reset to default")},expression:"{ content: $t('Reset to default') }"}],staticClass:"btn btn-default",class:{"disable-events":!t.showResetToDefault()},style:{opacity:t.showResetToDefault()?1:0},attrs:{type:"button"},on:{click:t.resetToDefault}},[a("i",{staticClass:"fa fa-refresh"})]),t._v(" "),a("button",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.$t("Undo")},expression:"{ content: $t('Undo') }"}],staticClass:"btn btn-primary",class:{"disable-events":!t.showUndo()},style:{opacity:t.showUndo()?1:0},attrs:{type:"button"},on:{click:t.resetToInitial}},[a("i",{staticClass:"fa fa-undo"})]),t._v(" "),t.hasHelp()?a("div",{directives:[{name:"tooltip",rawName:"v-tooltip",value:{content:t.getHelp(),trigger:"hover click"},expression:"{content: getHelp(), trigger: 'hover click'}"}],staticClass:"fa fa-fw fa-lg fa-question-circle"}):t._e()])])}),[],!1,null,"abd58c08",null).exports},2872:(t,e,a)=>{"use strict";a.r(e),a.d(e,{default:()=>c});function n(t,e){return function(t){if(Array.isArray(t))return t}(t)||function(t,e){var a=null==t?null:"undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(null==a)return;var n,s,i=[],r=!0,o=!1;try{for(a=a.call(t);!(r=(n=a.next()).done)&&(i.push(n.value),!e||i.length!==e);r=!0);}catch(t){o=!0,s=t}finally{try{r||null==a.return||a.return()}finally{if(o)throw s}}return i}(t,e)||function(t,e){if(!t)return;if("string"==typeof t)return s(t,e);var a=Object.prototype.toString.call(t).slice(8,-1);"Object"===a&&t.constructor&&(a=t.constructor.name);if("Map"===a||"Set"===a)return Array.from(t);if("Arguments"===a||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(a))return s(t,e)}(t,e)||function(){throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}()}function s(t,e){(null==e||e>t.length)&&(e=t.length);for(var a=0,n=new Array(e);a{var r=n(1957),i="object"==typeof self&&self&&self.Object===Object&&self,o=r||i||Function("return this")();t.exports=o},6390:t=>{t.exports=function(t,e){if(("constructor"!==e||"function"!=typeof t[e])&&"__proto__"!=e)return t[e]}},619:t=>{t.exports=function(t){return this.__data__.set(t,"__lodash_hash_undefined__"),this}},2385:t=>{t.exports=function(t){return this.__data__.has(t)}},1814:t=>{t.exports=function(t){var e=-1,n=Array(t.size);return t.forEach((function(t){n[++e]=t})),n}},61:(t,e,n)=>{var r=n(6560),i=n(1275)(r);t.exports=i},1275:t=>{var e=Date.now;t.exports=function(t){var n=0,r=0;return function(){var i=e(),o=16-(i-r);if(r=i,o>0){if(++n>=800)return arguments[0]}else n=0;return t.apply(void 0,arguments)}}},7465:(t,e,n)=>{var r=n(8407);t.exports=function(){this.__data__=new r,this.size=0}},3779:t=>{t.exports=function(t){var e=this.__data__,n=e.delete(t);return this.size=e.size,n}},7599:t=>{t.exports=function(t){return this.__data__.get(t)}},4758:t=>{t.exports=function(t){return this.__data__.has(t)}},4309:(t,e,n)=>{var r=n(8407),i=n(7071),o=n(3369);t.exports=function(t,e){var n=this.__data__;if(n instanceof r){var a=n.__data__;if(!i||a.length<199)return a.push([t,e]),this.size=++n.size,this;n=this.__data__=new o(a)}return n.set(t,e),this.size=n.size,this}},346:t=>{var e=Function.prototype.toString;t.exports=function(t){if(null!=t){try{return e.call(t)}catch(t){}try{return t+""}catch(t){}}return""}},5703:t=>{t.exports=function(t){return function(){return t}}},7813:t=>{t.exports=function(t,e){return t===e||t!=t&&e!=e}},6557:t=>{t.exports=function(t){return t}},5694:(t,e,n)=>{var r=n(9454),i=n(7005),o=Object.prototype,a=o.hasOwnProperty,s=o.propertyIsEnumerable,u=r(function(){return arguments}())?r:function(t){return i(t)&&a.call(t,"callee")&&!s.call(t,"callee")};t.exports=u},1469:t=>{var e=Array.isArray;t.exports=e},8612:(t,e,n)=>{var r=n(3560),i=n(1780);t.exports=function(t){return null!=t&&i(t.length)&&!r(t)}},9246:(t,e,n)=>{var r=n(8612),i=n(7005);t.exports=function(t){return i(t)&&r(t)}},4144:(t,e,n)=>{t=n.nmd(t);var r=n(5639),i=n(5062),o=e&&!e.nodeType&&e,a=o&&t&&!t.nodeType&&t,s=a&&a.exports===o?r.Buffer:void 0,u=(s?s.isBuffer:void 0)||i;t.exports=u},8446:(t,e,n)=>{var r=n(939);t.exports=function(t,e){return r(t,e)}},3560:(t,e,n)=>{var r=n(4239),i=n(3218);t.exports=function(t){if(!i(t))return!1;var e=r(t);return"[object Function]"==e||"[object GeneratorFunction]"==e||"[object AsyncFunction]"==e||"[object Proxy]"==e}},1780:t=>{t.exports=function(t){return"number"==typeof t&&t>-1&&t%1==0&&t<=9007199254740991}},3218:t=>{t.exports=function(t){var e=typeof t;return null!=t&&("object"==e||"function"==e)}},7005:t=>{t.exports=function(t){return null!=t&&"object"==typeof t}},8630:(t,e,n)=>{var r=n(4239),i=n(5924),o=n(7005),a=Function.prototype,s=Object.prototype,u=a.toString,c=s.hasOwnProperty,l=u.call(Object);t.exports=function(t){if(!o(t)||"[object Object]"!=r(t))return!1;var e=i(t);if(null===e)return!0;var n=c.call(e,"constructor")&&e.constructor;return"function"==typeof n&&n instanceof n&&u.call(n)==l}},6719:(t,e,n)=>{var r=n(8749),i=n(1717),o=n(1167),a=o&&o.isTypedArray,s=a?i(a):r;t.exports=s},3674:(t,e,n)=>{var r=n(4636),i=n(280),o=n(8612);t.exports=function(t){return o(t)?r(t):i(t)}},1704:(t,e,n)=>{var r=n(4636),i=n(313),o=n(8612);t.exports=function(t){return o(t)?r(t,!0):i(t)}},6486:function(t,e,n){var r;t=n.nmd(t),function(){var i,o="Expected a function",a="__lodash_hash_undefined__",s="__lodash_placeholder__",u=16,c=32,l=64,f=128,p=256,d=1/0,h=9007199254740991,v=NaN,m=4294967295,g=[["ary",f],["bind",1],["bindKey",2],["curry",8],["curryRight",u],["flip",512],["partial",c],["partialRight",l],["rearg",p]],y="[object Arguments]",b="[object Array]",_="[object Boolean]",w="[object Date]",x="[object Error]",O="[object Function]",S="[object GeneratorFunction]",C="[object Map]",E="[object Number]",T="[object Object]",k="[object Promise]",$="[object RegExp]",A="[object Set]",j="[object String]",D="[object Symbol]",L="[object WeakMap]",M="[object ArrayBuffer]",N="[object DataView]",P="[object Float32Array]",I="[object Float64Array]",R="[object Int8Array]",F="[object Int16Array]",B="[object Int32Array]",z="[object Uint8Array]",V="[object Uint8ClampedArray]",H="[object Uint16Array]",U="[object Uint32Array]",W=/\b__p \+= '';/g,q=/\b(__p \+=) '' \+/g,G=/(__e\(.*?\)|\b__t\)) \+\n'';/g,X=/&(?:amp|lt|gt|quot|#39);/g,Y=/[&<>"']/g,K=RegExp(X.source),J=RegExp(Y.source),Z=/<%-([\s\S]+?)%>/g,Q=/<%([\s\S]+?)%>/g,tt=/<%=([\s\S]+?)%>/g,et=/\.|\[(?:[^[\]]*|(["'])(?:(?!\1)[^\\]|\\.)*?\1)\]/,nt=/^\w*$/,rt=/[^.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|$))/g,it=/[\\^$.*+?()[\]{}|]/g,ot=RegExp(it.source),at=/^\s+/,st=/\s/,ut=/\{(?:\n\/\* \[wrapped with .+\] \*\/)?\n?/,ct=/\{\n\/\* \[wrapped with (.+)\] \*/,lt=/,? & /,ft=/[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g,pt=/[()=,{}\[\]\/\s]/,dt=/\\(\\)?/g,ht=/\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/g,vt=/\w*$/,mt=/^[-+]0x[0-9a-f]+$/i,gt=/^0b[01]+$/i,yt=/^\[object .+?Constructor\]$/,bt=/^0o[0-7]+$/i,_t=/^(?:0|[1-9]\d*)$/,wt=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,xt=/($^)/,Ot=/['\n\r\u2028\u2029\\]/g,St="\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff",Ct="\\u2700-\\u27bf",Et="a-z\\xdf-\\xf6\\xf8-\\xff",Tt="A-Z\\xc0-\\xd6\\xd8-\\xde",kt="\\ufe0e\\ufe0f",$t="\\xac\\xb1\\xd7\\xf7\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf\\u2000-\\u206f \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000",At="['’]",jt="[\\ud800-\\udfff]",Dt="["+$t+"]",Lt="["+St+"]",Mt="\\d+",Nt="[\\u2700-\\u27bf]",Pt="["+Et+"]",It="[^\\ud800-\\udfff"+$t+Mt+Ct+Et+Tt+"]",Rt="\\ud83c[\\udffb-\\udfff]",Ft="[^\\ud800-\\udfff]",Bt="(?:\\ud83c[\\udde6-\\uddff]){2}",zt="[\\ud800-\\udbff][\\udc00-\\udfff]",Vt="["+Tt+"]",Ht="(?:"+Pt+"|"+It+")",Ut="(?:"+Vt+"|"+It+")",Wt="(?:['’](?:d|ll|m|re|s|t|ve))?",qt="(?:['’](?:D|LL|M|RE|S|T|VE))?",Gt="(?:"+Lt+"|"+Rt+")"+"?",Xt="[\\ufe0e\\ufe0f]?",Yt=Xt+Gt+("(?:\\u200d(?:"+[Ft,Bt,zt].join("|")+")"+Xt+Gt+")*"),Kt="(?:"+[Nt,Bt,zt].join("|")+")"+Yt,Jt="(?:"+[Ft+Lt+"?",Lt,Bt,zt,jt].join("|")+")",Zt=RegExp(At,"g"),Qt=RegExp(Lt,"g"),te=RegExp(Rt+"(?="+Rt+")|"+Jt+Yt,"g"),ee=RegExp([Vt+"?"+Pt+"+"+Wt+"(?="+[Dt,Vt,"$"].join("|")+")",Ut+"+"+qt+"(?="+[Dt,Vt+Ht,"$"].join("|")+")",Vt+"?"+Ht+"+"+Wt,Vt+"+"+qt,"\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])","\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])",Mt,Kt].join("|"),"g"),ne=RegExp("[\\u200d\\ud800-\\udfff"+St+kt+"]"),re=/[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/,ie=["Array","Buffer","DataView","Date","Error","Float32Array","Float64Array","Function","Int8Array","Int16Array","Int32Array","Map","Math","Object","Promise","RegExp","Set","String","Symbol","TypeError","Uint8Array","Uint8ClampedArray","Uint16Array","Uint32Array","WeakMap","_","clearTimeout","isFinite","parseInt","setTimeout"],oe=-1,ae={};ae[P]=ae[I]=ae[R]=ae[F]=ae[B]=ae[z]=ae[V]=ae[H]=ae[U]=!0,ae[y]=ae[b]=ae[M]=ae[_]=ae[N]=ae[w]=ae[x]=ae[O]=ae[C]=ae[E]=ae[T]=ae[$]=ae[A]=ae[j]=ae[L]=!1;var se={};se[y]=se[b]=se[M]=se[N]=se[_]=se[w]=se[P]=se[I]=se[R]=se[F]=se[B]=se[C]=se[E]=se[T]=se[$]=se[A]=se[j]=se[D]=se[z]=se[V]=se[H]=se[U]=!0,se[x]=se[O]=se[L]=!1;var ue={"\\":"\\","'":"'","\n":"n","\r":"r","\u2028":"u2028","\u2029":"u2029"},ce=parseFloat,le=parseInt,fe="object"==typeof n.g&&n.g&&n.g.Object===Object&&n.g,pe="object"==typeof self&&self&&self.Object===Object&&self,de=fe||pe||Function("return this")(),he=e&&!e.nodeType&&e,ve=he&&t&&!t.nodeType&&t,me=ve&&ve.exports===he,ge=me&&fe.process,ye=function(){try{var t=ve&&ve.require&&ve.require("util").types;return t||ge&&ge.binding&&ge.binding("util")}catch(t){}}(),be=ye&&ye.isArrayBuffer,_e=ye&&ye.isDate,we=ye&&ye.isMap,xe=ye&&ye.isRegExp,Oe=ye&&ye.isSet,Se=ye&&ye.isTypedArray;function Ce(t,e,n){switch(n.length){case 0:return t.call(e);case 1:return t.call(e,n[0]);case 2:return t.call(e,n[0],n[1]);case 3:return t.call(e,n[0],n[1],n[2])}return t.apply(e,n)}function Ee(t,e,n,r){for(var i=-1,o=null==t?0:t.length;++i-1},Xn.prototype.set=function(t,e){var n=this.__data__,r=ir(n,t);return r<0?(++this.size,n.push([t,e])):n[r][1]=e,this},Yn.prototype.clear=function(){this.size=0,this.__data__={hash:new Gn,map:new(Tn||Xn),string:new Gn}},Yn.prototype.delete=function(t){var e=fo(this,t).delete(t);return this.size-=e?1:0,e},Yn.prototype.get=function(t){return fo(this,t).get(t)},Yn.prototype.has=function(t){return fo(this,t).has(t)},Yn.prototype.set=function(t,e){var n=fo(this,t),r=n.size;return n.set(t,e),this.size+=n.size==r?0:1,this},Kn.prototype.add=Kn.prototype.push=function(t){return this.__data__.set(t,a),this},Kn.prototype.has=function(t){return this.__data__.has(t)},Jn.prototype.clear=function(){this.__data__=new Xn,this.size=0},Jn.prototype.delete=function(t){var e=this.__data__,n=e.delete(t);return this.size=e.size,n},Jn.prototype.get=function(t){return this.__data__.get(t)},Jn.prototype.has=function(t){return this.__data__.has(t)},Jn.prototype.set=function(t,e){var n=this.__data__;if(n instanceof Xn){var r=n.__data__;if(!Tn||r.length<199)return r.push([t,e]),this.size=++n.size,this;n=this.__data__=new Yn(r)}return n.set(t,e),this.size=n.size,this};var hr=Ni(xr),vr=Ni(Or,!0);function mr(t,e){var n=!0;return hr(t,(function(t,r,i){return n=!!e(t,r,i)})),n}function gr(t,e,n){for(var r=-1,o=t.length;++r