From 60d6da0e662c761c5b39a42e25f5ddd4ead47b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20K=C5=82opotek=20-=20INTERDUO?= Date: Mon, 5 Jun 2023 22:11:20 +0200 Subject: [PATCH 1/3] enhancement: added support for user API keys (db/frontend part) --- doc/ChangeLog | 1 + doc/lms.mysql | 4 +- doc/lms.pgsql | 4 +- lib/LMS.class.php | 6 ++ lib/LMSDB_common.class.php | 2 +- lib/LMSManagers/LMSUserManager.php | 90 ++++++++++++++------- lib/LMSManagers/LMSUserManagerInterface.php | 2 + lib/common.php | 8 +- lib/locale/pl_PL/strings.php | 10 ++- lib/upgradedb/mysql.2023060100.php | 53 ++++++++++++ lib/upgradedb/postgres.2023060100.php | 53 ++++++++++++ modules/useradd.php | 8 ++ modules/userpasswd.php | 61 ++++++++------ templates/default/user/useradd.html | 24 ++++++ templates/default/user/usereditbox.html | 11 +++ templates/default/user/userinfobox.html | 18 +++++ templates/default/user/userlist.html | 3 + templates/default/user/userpasswd.html | 45 +++++++---- 18 files changed, 329 insertions(+), 74 deletions(-) create mode 100644 lib/upgradedb/mysql.2023060100.php create mode 100644 lib/upgradedb/postgres.2023060100.php diff --git a/doc/ChangeLog b/doc/ChangeLog index e725e478af..656bd9af29 100644 --- a/doc/ChangeLog +++ b/doc/ChangeLog @@ -514,6 +514,7 @@ version 28-git (????-??-??): specific sms-like section configuration variable names [chilan] - improvement: send document confirm notifications if document is created with 'closed' status [chilan] + - enhancement: added support for user API keys [interduo] version 27.0 (2021-08-20): diff --git a/doc/lms.mysql b/doc/lms.mysql index eb779b4b8f..28f3c52433 100644 --- a/doc/lms.mysql +++ b/doc/lms.mysql @@ -225,6 +225,8 @@ CREATE TABLE users ( persistentsettings mediumtext NOT NULL DEFAULT '', twofactorauth smallint NOT NULL DEFAULT 0, twofactorauthsecretkey varchar(255) DEFAULT NULL, + api smallint DEFAULT 0 NOT NULL, + apikey text DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY login (login) ) ENGINE=InnoDB; @@ -4332,4 +4334,4 @@ INSERT INTO netdevicemodels (name, alternative_name, netdeviceproducerid) VALUES ('XR7', 'XR7 MINI PCI PCBA', 2), ('XR9', 'MINI PCI 600MW 900MHZ', 2); -INSERT INTO dbinfo (keytype, keyvalue) VALUES ('dbversion', '2023053000'); +INSERT INTO dbinfo (keytype, keyvalue) VALUES ('dbversion', '2023060100'); diff --git a/doc/lms.pgsql b/doc/lms.pgsql index e18aebc7c7..45c3621ff3 100644 --- a/doc/lms.pgsql +++ b/doc/lms.pgsql @@ -38,6 +38,8 @@ CREATE TABLE users ( persistentsettings text NOT NULL DEFAULT '', twofactorauth smallint NOT NULL DEFAULT 0, twofactorauthsecretkey varchar(255) DEFAULT NULL, + api smallint DEFAULT 0 NOT NULL, + apikey text DEFAULT NULL, PRIMARY KEY (id), UNIQUE (login) ); @@ -4377,6 +4379,6 @@ INSERT INTO netdevicemodels (name, alternative_name, netdeviceproducerid) VALUES ('XR7', 'XR7 MINI PCI PCBA', 2), ('XR9', 'MINI PCI 600MW 900MHZ', 2); -INSERT INTO dbinfo (keytype, keyvalue) VALUES ('dbversion', '2023053000'); +INSERT INTO dbinfo (keytype, keyvalue) VALUES ('dbversion', '2023060100'); COMMIT; diff --git a/lib/LMS.class.php b/lib/LMS.class.php index d9977ddd22..fcd1e8b264 100644 --- a/lib/LMS.class.php +++ b/lib/LMS.class.php @@ -506,6 +506,12 @@ public function isUserNetworkPasswordSet($id) return $manager->isUserNetworkPasswordSet($id); } + public function hasUserApiKeySet($id) + { + $manager = $this->getUserManager(); + return $manager->hasUserApiKeySet($id); + } + /* * Customers functions */ diff --git a/lib/LMSDB_common.class.php b/lib/LMSDB_common.class.php index 9f9728f876..c0da8c52da 100644 --- a/lib/LMSDB_common.class.php +++ b/lib/LMSDB_common.class.php @@ -25,7 +25,7 @@ */ // here should be always the newest version of database! -define('DBVERSION', '2023053000'); +define('DBVERSION', '2023060100'); /** * diff --git a/lib/LMSManagers/LMSUserManager.php b/lib/LMSManagers/LMSUserManager.php index e8ed46d070..6d9b6b66bb 100644 --- a/lib/LMSManagers/LMSUserManager.php +++ b/lib/LMSManagers/LMSUserManager.php @@ -39,28 +39,41 @@ class LMSUserManager extends LMSManager implements LMSUserManagerInterface */ public function setUserPassword($id, $passwd, $net = false) { - if ($net) { - $args = array( - 'netpasswd' => empty($passwd) ? null : $passwd, - SYSLOG::RES_USER => $id - ); - $result = $this->db->Execute( - 'UPDATE users SET netpasswd = ? - WHERE id = ?', - array_values($args) - ); - } else { - $args = array( - 'passwd' => password_hash($passwd, PASSWORD_DEFAULT), - 'passwdforcechange' => 0, - SYSLOG::RES_USER => $id - ); - $result = $this->db->Execute( - 'UPDATE users SET passwd = ?, passwdlastchange = ?NOW?, passwdforcechange = ? - WHERE id = ?', - array_values($args) - ); - $this->db->Execute('INSERT INTO passwdhistory (userid, hash) VALUES (?, ?)', array($id, password_hash($passwd, PASSWORD_DEFAULT))); + switch($net) { + case 2: + $args = array( + 'passwd' => empty($passwd) ? null : password_hash($passwd, PASSWORD_DEFAULT), + SYSLOG::RES_USER => $id + ); + $result = $this->db->Execute( + 'UPDATE users SET apikey = ? + WHERE id = ?', + array_values($args) + ); + break; + case 1: + $args = array( + 'netpasswd' => empty($passwd) ? null : $passwd, + SYSLOG::RES_USER => $id + ); + $result = $this->db->Execute( + 'UPDATE users SET netpasswd = ? + WHERE id = ?', + array_values($args) + ); + break; + default: + $args = array( + 'passwd' => password_hash($passwd, PASSWORD_DEFAULT), + 'passwdforcechange' => 0, + SYSLOG::RES_USER => $id + ); + $result = $this->db->Execute( + 'UPDATE users SET passwd = ?, passwdlastchange = ?NOW?, passwdforcechange = ? + WHERE id = ?', + array_values($args) + ); + $this->db->Execute('INSERT INTO passwdhistory (userid, hash) VALUES (?, ?)', array($id, password_hash($passwd, PASSWORD_DEFAULT))); } if ($result && $this->syslog) { unset($args['passwd']); @@ -206,7 +219,7 @@ public function getUserList($params = array()) $userlist = $this->db->GetAllByKey( 'SELECT id, login, name, phone, lastlogindate, lastloginip, passwdexpiration, passwdlastchange, access, - accessfrom, accessto, rname, twofactorauth' + accessfrom, accessto, rname, twofactorauth, api' . ' FROM ' . (isset($superuser) ? 'vallusers' : 'vusers') . ' WHERE deleted = 0' . (isset($userAccess) ? ' AND access = 1 AND accessfrom <= ?NOW? AND (accessto >=?NOW? OR accessto = 0)' : '' ) @@ -274,6 +287,8 @@ public function userAdd($user) 'position' => $user['position'], 'ntype' => !empty($user['ntype']) ? $user['ntype'] : null, 'phone' => !empty($user['phone']) ? $user['phone'] : null, + 'api' => empty($user['api']) ? 0 : 1, + 'apikey' => empty($user['apikey']) ? null : password_hash($user['apikey'], PASSWORD_DEFAULT), 'passwdforcechange' => isset($user['passwdforcechange']) ? 1 : 0, 'passwdexpiration' => !empty($user['passwdexpiration']) ? $user['passwdexpiration'] : 0, 'access' => !empty($user['access']) ? 1 : 0, @@ -284,9 +299,9 @@ public function userAdd($user) ); $user_inserted = $this->db->Execute( 'INSERT INTO users (login, firstname, lastname, issuer, email, passwd, netpasswd, rights, - hosts, trustedhosts, position, ntype, phone, + hosts, trustedhosts, position, ntype, phone, api, apikey, passwdforcechange, passwdexpiration, access, accessfrom, accessto, twofactorauth, twofactorauthsecretkey) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', array_values($args) ); if ($user_inserted) { @@ -507,6 +522,7 @@ public function userUpdate($user) 'position' => $user['position'], 'ntype' => !empty($user['ntype']) ? $user['ntype'] : null, 'phone' => !empty($user['phone']) ? $user['phone'] : null, + 'api' => empty($user['api']) ? 0 : 1, 'passwdforcechange' => isset($user['passwdforcechange']) ? 1 : 0, 'passwdexpiration' => !empty($user['passwdexpiration']) ? $user['passwdexpiration'] : 0, 'access' => !empty($user['access']) ? 1 : 0, @@ -518,7 +534,7 @@ public function userUpdate($user) ); $res = $this->db->Execute( 'UPDATE users SET login = ?, firstname = ?, lastname = ?, issuer = ?, email = ?, rights = ?, - hosts = ?, trustedhosts = ?, position = ?, ntype = ?, phone = ?, passwdforcechange = ?, passwdexpiration = ?, + hosts = ?, trustedhosts = ?, position = ?, ntype = ?, phone = ?, api = ?, passwdforcechange = ?, passwdexpiration = ?, access = ?, accessfrom = ?, accessto = ?, twofactorauth = ?, twofactorauthsecretkey = ? WHERE id = ?', array_values($args) @@ -663,15 +679,20 @@ public function PasswdExistsInHistory($id, $passwd) public function checkPassword($password, $net = false) { - if ($net) { + if ($net == 1) { return $this->db->GetOne( 'SELECT netpasswd FROM users WHERE id = ?', array(Auth::GetCurrentUser()) ) == $password; } else { $dbpasswd = $this->db->GetOne( - 'SELECT passwd FROM users WHERE id = ?', - array(Auth::GetCurrentUser()) + 'SELECT ? FROM users + WHERE + id = ?', + array( + ($net == 2) ? 'apikey' : 'passwd', + Auth::GetCurrentUser(), + ) ); return password_verify($password, $dbpasswd); } @@ -687,4 +708,15 @@ public function isUserNetworkPasswordSet($id) ) ) == 1; } + + public function hasUserApiKeySet($id) + { + return $this->db->GetOne( + 'SELECT 1 FROM users + WHERE + id = ? + AND apikey IS NOT NULL', + array($id) + ) == 1; + } } diff --git a/lib/LMSManagers/LMSUserManagerInterface.php b/lib/LMSManagers/LMSUserManagerInterface.php index 40a090601e..4347f0a49c 100644 --- a/lib/LMSManagers/LMSUserManagerInterface.php +++ b/lib/LMSManagers/LMSUserManagerInterface.php @@ -69,4 +69,6 @@ public function PasswdExistsInHistory($id, $passwd); public function checkPassword($password, $net = false); public function isUserNetworkPasswordSet($id); + + public function hasUserApiKeySet($id); } diff --git a/lib/common.php b/lib/common.php index a7eb84ba09..5672ec6894 100644 --- a/lib/common.php +++ b/lib/common.php @@ -1100,10 +1100,12 @@ function is_natural($var) return preg_match('/^[1-9][0-9]*$/', $var); } -function check_password_strength($password) +function check_password_strength($password, $passlength = 8) { - return (preg_match('/[a-z]/', $password) && preg_match('/[A-Z]/', $password) - && preg_match('/[0-9]/', $password) && mb_strlen($password) >= 8); + return preg_match('/[a-z]/', $password) + && preg_match('/[A-Z]/', $password) + && preg_match('/[0-9]/', $password) + && mb_strlen($password) > ($passlength); } function access_denied() diff --git a/lib/locale/pl_PL/strings.php b/lib/locale/pl_PL/strings.php index 676c42b3fa..d2fc44475c 100644 --- a/lib/locale/pl_PL/strings.php +++ b/lib/locale/pl_PL/strings.php @@ -1887,6 +1887,14 @@ $_LANG['Sale Registry'] = 'Rejestr sprzedaży'; $_LANG['Sale Registry for period $a - $b'] = 'Rejestr sprzedaży za okres $a - $b'; $_LANG['Save'] = 'Zapisz'; +$_LANG['Please input API Key'] = 'Podaj klucz API'; +$_LANG['API access'] = 'Dostęp API'; +$_LANG['API access could not be turned on when API key is empty!'] = 'Dostęp API nie może być włączony gdy klucz API jest pusty!'; +$_LANG['Enter new API key'] = 'Wprowadź nowy klucz API'; +$_LANG['Change API key'] = 'Zmień klucz API'; +$_LANG['Repeat new API key'] = 'Powtórz nowy klucz API'; +$_LANG['API keys do not match!'] = 'Klucze API nie są takie same!'; +$_LANG['API key should contain at least one capital letter, one lower case letter, one digit and should consist of at least 16 and maximum 8192 characters!'] = 'Klucz API powinien zawierać co najmniej jedną dużą, małą literę, cyfrę, co najmniej 16 i maksymalnie 8192 znaki!'; $_LANG['Save & Print'] = 'Zapisz i drukuj'; $_LANG['Scan'] = 'Skanuj'; $_LANG['Search'] = 'Szukaj'; @@ -5673,7 +5681,7 @@ $_LANG['Network password'] = 'Hasło sieciowe'; $_LANG['Network Password'] = 'Hasło sieciowe'; $_LANG['Change your network password'] = 'Zmiana Twojego hasła sieciowego'; -$_LANG['Repeat network password'] = 'Powtórz hasło sieciowe'; +$_LANG['Repeat new network password'] = 'Powtórz nowe hasło sieciowe'; $_LANG['Network password can be used to authenticate users via Radius server.'] = 'Hasło sieciowe może być używane do uwierzytelniania użytkowników w oparciu o serwer Radius.'; $_LANG['Change network password'] = 'Zmiana hasła sieciowego'; diff --git a/lib/upgradedb/mysql.2023060100.php b/lib/upgradedb/mysql.2023060100.php new file mode 100644 index 0000000000..09949b928e --- /dev/null +++ b/lib/upgradedb/mysql.2023060100.php @@ -0,0 +1,53 @@ +BeginTrans(); + +$this->Execute("ALTER TABLE users ADD COLUMN api smallint DEFAULT 0 NOT NULL"); +$this->Execute("ALTER TABLE users ADD COLUMN apikey text DEFAULT NULL"); + +$this->Execute("DROP VIEW vallusers"); +$this->Execute("DROP VIEW vusers"); + +$this->Execute(" + CREATE VIEW vusers AS + SELECT u.*, CONCAT(u.firstname, ' ', u.lastname) AS name, CONCAT(u.lastname, ' ', u.firstname) AS rname + FROM users u + LEFT JOIN userdivisions ud ON u.id = ud.userid + WHERE lms_current_user() = 0 OR ud.divisionid IN ( + SELECT ud2.divisionid + FROM userdivisions ud2 + WHERE ud2.userid = lms_current_user() + ) + GROUP BY u.id + "); + +$this->Execute(" + CREATE VIEW vallusers AS + SELECT *, CONCAT(firstname, ' ', lastname) AS name, CONCAT(lastname, ' ', firstname) AS rname + FROM users + "); + +$this->Execute("UPDATE dbinfo SET keyvalue = ? WHERE keytype = ?", array('2023060100', 'dbversion')); + +$this->CommitTrans(); diff --git a/lib/upgradedb/postgres.2023060100.php b/lib/upgradedb/postgres.2023060100.php new file mode 100644 index 0000000000..89e8e34d68 --- /dev/null +++ b/lib/upgradedb/postgres.2023060100.php @@ -0,0 +1,53 @@ +BeginTrans(); + +$this->Execute("ALTER TABLE users ADD COLUMN api smallint DEFAULT 0 NOT NULL"); +$this->Execute("ALTER TABLE users ADD COLUMN apikey text DEFAULT NULL"); + +$this->Execute("DROP VIEW vallusers"); +$this->Execute("DROP VIEW vusers"); + +$this->Execute(" + CREATE VIEW vusers AS + SELECT u.*, (u.firstname || ' ' || u.lastname) AS name, (u.lastname || ' ' || u.firstname) AS rname + FROM users u + LEFT JOIN userdivisions ud ON u.id = ud.userid + WHERE lms_current_user() = 0 OR ud.divisionid IN ( + SELECT ud2.divisionid + FROM userdivisions ud2 + WHERE ud2.userid = lms_current_user() + ) + GROUP BY u.id +"); + +$this->Execute(" + CREATE VIEW vallusers AS + SELECT *, (firstname || ' ' || lastname) AS name, (lastname || ' ' || firstname) AS rname + FROM users +"); + +$this->Execute("UPDATE dbinfo SET keyvalue = ? WHERE keytype = ?", array('2023060100', 'dbversion')); + +$this->CommitTrans(); diff --git a/modules/useradd.php b/modules/useradd.php index 2ed0501051..87d267cd80 100644 --- a/modules/useradd.php +++ b/modules/useradd.php @@ -92,6 +92,14 @@ } } + if (!empty($useradd['apikey']) && !check_password_strength($useradd['apikey'])) { + $error['apikey'] = trans('The password should contain at least one capital letter, one lower case letter, one digit and should consist of at least 16 characters!'); + } + + if (!empty($useradd['api']) && empty($useradd['apikey'])) { + $error['apikey'] = $error['api'] = trans('API access could not be turned on when API key is empty!'); + } + if (!empty($useradd['accessfrom'])) { $accessfrom = date_to_timestamp($useradd['accessfrom']); if (empty($accessfrom)) { diff --git a/modules/userpasswd.php b/modules/userpasswd.php index 5e16815eba..6b8a3a9c13 100644 --- a/modules/userpasswd.php +++ b/modules/userpasswd.php @@ -27,32 +27,44 @@ $id = (isset($_GET['id'])) ? $_GET['id'] : Auth::GetCurrentUser(); if ($LMS->UserExists($id)) { - $net = isset($_GET['net']) ? 1 : 0; + $net = empty($_GET['net']) ? 0 : intval($_GET['net']); if (isset($_POST['password'])) { $passwd = $_POST['password']; - if ($net) { - if ($id == Auth::GetCurrentUser() - && $LMS->isUserNetworkPasswordSet($id) && !$LMS->checkPassword($passwd['currentpasswd'], true)) { - $error['currentpasswd'] = trans('Wrong current password!'); - } elseif ($passwd['passwd'] != $passwd['confirm']) { - $error['passwd'] = trans('Passwords do not match!'); - } elseif ($passwd['passwd'] != '' && !check_password_strength($passwd['passwd'])) { - $error['passwd'] = trans('The password should contain at least one capital letter, one lower case letter, one digit and should consist of at least 8 characters!'); - } - } else { - if ($id == Auth::GetCurrentUser() && !$LMS->checkPassword($passwd['currentpasswd'])) { - $error['currentpasswd'] = trans('Wrong current password!'); - } elseif ($passwd['passwd'] == '' || $passwd['confirm'] == '') { - $error['passwd'] = trans('Empty passwords are not allowed!').'
'; - } elseif ($passwd['passwd'] != $passwd['confirm']) { - $error['passwd'] = trans('Passwords do not match!'); - } elseif (!check_password_strength($passwd['passwd'])) { - $error['passwd'] = trans('The password should contain at least one capital letter, one lower case letter, one digit and should consist of at least 8 characters!'); - } elseif ($LMS->PasswdExistsInHistory($id, $passwd['passwd'])) { - $error['passwd'] = trans('You already used this password!'); - } + switch ($net) { + case 2: + if ($id == Auth::GetCurrentUser() && $LMS->hasUserApiKeySet($id) + && !$LMS->checkPassword($passwd['currentpasswd'])) { + $error['currentpasswd'] = trans('Wrong current password!'); + } elseif ($passwd['passwd'] != $passwd['confirm']) { + $error['passwd'] = $error['confirm'] = trans('API keys do not match!'); + } elseif ($passwd['passwd'] != '' && !check_password_strength($passwd['passwd'], 16)) { + $error['passwd'] = trans('API key should contain at least one capital letter, one lower case letter, one digit and should consist of at least 16 and maximum 8192 characters!'); + } + break; + case 1: + if ($id == Auth::GetCurrentUser() + && $LMS->isUserNetworkPasswordSet($id) && !$LMS->checkPassword($passwd['currentpasswd'], 1)) { + $error['currentpasswd'] = trans('Wrong current password!'); + } elseif ($passwd['passwd'] != $passwd['confirm']) { + $error['passwd'] = trans('Passwords do not match!'); + } elseif ($passwd['passwd'] != '' && !check_password_strength($passwd['passwd'])) { + $error['passwd'] = trans('The password should contain at least one capital letter, one lower case letter, one digit and should consist of at least 8 characters!'); + } + break; + default: + if ($id == Auth::GetCurrentUser() && !$LMS->checkPassword($passwd['currentpasswd'])) { + $error['currentpasswd'] = trans('Wrong current password!'); + } elseif ($passwd['passwd'] == '' || $passwd['confirm'] == '') { + $error['passwd'] = trans('Empty passwords are not allowed!') . '
'; + } elseif ($passwd['passwd'] != $passwd['confirm']) { + $error['passwd'] = trans('Passwords do not match!'); + } elseif (!check_password_strength($passwd['passwd'])) { + $error['passwd'] = trans('The password should contain at least one capital letter, one lower case letter, one digit and should consist of at least 8 characters!'); + } elseif ($LMS->PasswdExistsInHistory($id, $passwd['passwd'])) { + $error['passwd'] = trans('You already used this password!'); + } } if (!$error) { @@ -62,10 +74,13 @@ } $passwd['id'] = $id; - if ($net) { + if ($net == 1) { $passwd['netpasswd'] = $LMS->isUserNetworkPasswordSet($id); + } elseif ($net == 2) { + $passwd['apikey'] = $LMS->hasUserApiKeySet($id); } + $layout['pagetitle'] = trans('Password Change for User $a', $DB->GetOne('SELECT name FROM vusers WHERE id = ?', array($id))); $SMARTY->assign('error', $error); diff --git a/templates/default/user/useradd.html b/templates/default/user/useradd.html index 8463d6d93a..2e02c97575 100644 --- a/templates/default/user/useradd.html +++ b/templates/default/user/useradd.html @@ -206,6 +206,20 @@

{$layout.pagetitle}

{hint mode="toggle" content="Network password can be used to authenticate users via Radius server."} + + + {icon name="password"} + {trans("API Key:")} + + + + + {hint mode='toggle' content='Please input API Key'} + +