diff --git a/src/etc/inc/auth.inc b/src/etc/inc/auth.inc index a55bced8c3d..90c2aebfab6 100644 --- a/src/etc/inc/auth.inc +++ b/src/etc/inc/auth.inc @@ -216,31 +216,6 @@ function userIsAdmin($username) return userHasPrivilege($user, 'page-all'); } -function auth_get_shells($uid = 0) -{ - $shells = ['' => '/usr/sbin/nologin']; - - if ($uid == 0) { - $shells = ['' => '/usr/local/sbin/opnsense-shell']; - } - - $etc_shells = @file_get_contents('/etc/shells'); - if (!empty($etc_shells)) { - $etc_shells = explode("\n", $etc_shells); - foreach ($etc_shells as $shell) { - $shell = trim($shell); - if ( - !empty($shell) && strpos($shell, '#') !== 0 && - strpos($shell, '/usr/local/sbin/opnsense-') !== 0 - ) { - $shells[$shell] = $shell; - } - } - } - - return $shells; -} - function local_sync_accounts() { global $config; @@ -428,17 +403,6 @@ function local_user_set(&$user, $force_password = false, $userattrs = null) mwexecf('/usr/sbin/pw %s %s', array($lock_account, $user_name), true); } -function local_user_del($user) -{ - /* remove all memberships */ - local_user_set_groups($user); - - /* delete from pw db */ - mwexecf('/usr/sbin/pw userdel -n %s -r', $user['name']); - - /* Delete user from groups needs a call to write_config() */ - local_group_del_user($user); -} function local_user_set_password(&$user, $password = null) { @@ -508,77 +472,6 @@ function local_user_get_groups($user) return $groups; } -function local_user_set_groups($user, $new_groups = null) -{ - global $config, $groupindex; - - if (!isset($config['system']['group'])) { - return; - } - - $cur_groups = local_user_get_groups($user); - $mod_groups = array(); - - if (!is_array($new_groups)) { - $new_groups = array(); - } - - if (!is_array($cur_groups)) { - $cur_groups = array(); - } - - /* determine which memberships to add */ - foreach ($new_groups as $groupname) { - if (in_array($groupname, $cur_groups) || !isset($groupindex[$groupname])) { - // continue if group is already in current list or the groupname is invalid - continue; - } - $group = &config_read_array('system', 'group', $groupindex[$groupname]); - $group['member'][] = $user['uid']; - $mod_groups[] = $group; - } - - /* determine which memberships to remove */ - foreach ($cur_groups as $groupname) { - if (in_array($groupname, $new_groups)) { - continue; - } - if (!isset($config['system']['group'][$groupindex[$groupname]])) { - continue; - } - $group = &config_read_array('system', 'group', $groupindex[$groupname]); - if (is_array($group['member'])) { - $index = array_search($user['uid'], $group['member']); - array_splice($group['member'], $index, 1); - $mod_groups[] = $group; - } - } - - /* sync all modified groups */ - foreach ($mod_groups as $group) { - local_group_set($group); - } -} - -function local_group_del_user($user) -{ - global $config; - - if (!isset($config['system']['group'])) { - return; - } - - foreach ($config['system']['group'] as $group) { - if (isset($group['member'])) { - foreach ($group['member'] as $idx => $uid) { - if ($user['uid'] == $uid) { - unset($config['system']['group']['member'][$idx]); - } - } - } - } -} - function local_group_set($group) { if (!isset($group['name']) || !isset($group['gid'])) { @@ -607,12 +500,6 @@ function local_group_set($group) mwexecf('/usr/sbin/pw %s %s -g %s -M %s', array($group_op, $group_name, $group_gid, $group_members)); } -function local_group_del($group) -{ - /* delete from group db */ - mwexecf('/usr/sbin/pw groupdel %s', $group['name']); -} - /** * @param $name string name of the authentication system configured on the authentication server page or 'Local Database' for local authentication diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/GroupController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/GroupController.php new file mode 100644 index 00000000000..e5137f79e1e --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/GroupController.php @@ -0,0 +1,95 @@ +searchBase('group'); + } + + public function getAction($uuid = null) + { + return $this->getBase('group', 'group', $uuid); + } + + public function addAction() + { + $result = $this->addBase('group', 'group'); + if ($result['result'] != 'failed') { + $data = $this->request->getPost(static::$internalModelName); + (new Backend())->configdRun('auth sync group '. $data['name']); + } + return $result; + } + + public function setAction($uuid = null) + { + $result = $this->setBase('group', 'group', $uuid); + if ($result['result'] != 'failed') { + $data = $this->request->getPost(static::$internalModelName); + (new Backend())->configdRun('auth sync group '. $data['name']); + } + return $result; + } + + public function delAction($uuid) + { + $groupname = null; + if ($this->request->isPost()) { + Config::getInstance()->lock(); + $node = $this->getModel()->getNodeByReference('group.' . $uuid); + if ($node->scope == 'system') { + throw new UserException(sprintf(gettext("Not allowed to delete system group %s"), $node->name)); + } + if (!empty($node)) { + $groupname = (string)$node->name; + } + } + $result = $this->delBase('group', $uuid); + if ($groupname != null) { + (new Backend())->configdRun('auth sync group '. $groupname); + } + return $result; + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/PrivController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/PrivController.php new file mode 100644 index 00000000000..f293b40cfd6 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/PrivController.php @@ -0,0 +1,143 @@ +user->iterateItems() as $user) { + foreach (explode(',', $user->priv->getCurrentValue()) as $priv) { + if (!isset($userprivs[$priv])) { + $userprivs[$priv] = []; + } + $userprivs[$priv][] = (string)$user->name; + } + } + foreach ((new Group())->group->iterateItems() as $group) { + foreach (explode(',', $group->priv->getCurrentValue()) as $priv) { + if (!isset($groupprivs[$priv])) { + $groupprivs[$priv] = []; + } + $groupprivs[$priv][] = (string)$group->name; + } + } + + $records = []; + foreach ((new ACL())->getPrivList() as $auth => $props) { + $records[] = [ + 'id' => $auth, + 'name' => $props['name'], + 'match' => implode("\n", $props['match'] ?? []), + 'users' => $userprivs[$auth] ?? [], + 'groups' => $groupprivs[$auth] ?? [], + ]; + } + return $this->searchRecordsetBase($records); + } + + public function getItemAction($id) + { + $result = parent::getAction(); + if (isset($result['priv'])) { + $result['priv']['id'] = $id; + foreach ((new User())->user->iterateItems() as $uuid => $user) { + if ( + in_array($id, explode(',', $user->priv->getCurrentValue())) && + isset($result['priv']['users'][$uuid]) + ) { + $result['priv']['users'][$uuid]['selected'] = 1; + } + } + foreach ((new Group())->group->iterateItems() as $uuid => $group) { + if ( + in_array($id, explode(',', $group->priv->getCurrentValue())) && + isset($result['priv']['groups'][$uuid]) + ) { + $result['priv']['groups'][$uuid]['selected'] = 1; + } + } + } + + return $result; + } + + public function setItemAction($id) + { + if ($this->request->isPost()) { + Config::getInstance()->lock(); + } + $result = parent::setAction(); + if ($result['result'] != 'failed') { + $mdl = $this->getModel(); + $usermdl = new User(); + $groupmdl = new Group(); + foreach ([$usermdl->user, $groupmdl->group] as $topic) { + if ($topic == $usermdl->user) { + $uuids = explode(',', $mdl->users->getCurrentValue()); + } else { + $uuids = explode(',', $mdl->groups->getCurrentValue()); + } + foreach ($topic->iterateItems() as $uuid => $item) { + $privlist = array_filter(explode(',', $item->priv->getCurrentValue())); + if (!in_array($uuid, $uuids) && in_array($id, $privlist)) { + unset($privlist[array_search($id, $privlist)]); + } elseif (in_array($uuid, $uuids) && !in_array($id, $privlist)) { + $privlist[] = $id; + } else { + continue; + } + $item->priv = implode(',', $privlist); + } + } + $usermdl->serializeToConfig(false, true); + $groupmdl->serializeToConfig(false, true); + Config::getInstance()->save(); + } + return $result; + } + +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/UserController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/UserController.php new file mode 100644 index 00000000000..c7fa820367e --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/Api/UserController.php @@ -0,0 +1,256 @@ +object(); + return $config->system->hostname . '.' . $config->system->domain; + } + + protected function setBaseHook($node) + { + if (!empty($this->request->getPost(static::$internalModelName)) && $this->request->isPost()) { + /* XXX: Merge group membership, move to model when group model exists */ + $data = $this->request->getPost(static::$internalModelName); + if (!empty($data['group_memberships'])) { + $this_gids = explode(',', $data['group_memberships']); + $this_uid = (string)$node->uid; + foreach (Config::getInstance()->object()->system->children() as $tag => $node) { + if ($tag == 'group') { + $in_group = in_array((string)$node->gid, $this_gids); + if (isset($node->member)) { + for ($i = count($node->member) -1 ; $i >= 0 ; --$i) { + if ($node->member[$i] == $this_uid) { + if (!$in_group) { + unset($node->member[$i]); + } else { + continue 2; + } + } + } + } + if ($in_group) { + $node->addChild('member', $this_uid); + } + } + } + } + /* Password handling */ + if (!empty($data['scrambled_password']) || !empty($data['password'])) { + if (!empty($data['scrambled_password'])) { + /* generate a random password */ + $password = random_bytes(50); + /* XXX since PHP 8.2.18 we need to avoid NUL char */ + while (($i = strpos($password, "\0")) !== false) { + $password[$i] = random_bytes(1); + } + } else { + $password = $data['password']; + } + $hash = false; + $webgui = Config::getInstance()->object()->system->webgui; + if ( + !empty($webgui) && + !empty((string)$webgui->enable_password_policy_constraints) && + !empty((string)$webgui->password_policy_compliance) + ) { + /* compliance SHA-512 hashing */ + $process = proc_open( + '/usr/local/bin/openssl passwd -6 -stdin', + [['pipe', 'r'], ['pipe', 'w']], + $pipes + ); + if (is_resource($process)) { + fwrite($pipes[0], $password); + fclose($pipes[0]); + $hash = trim(stream_get_contents($pipes[1])); + fclose($pipes[1]); + proc_close($process); + } + } else { + $hash = password_hash($password, PASSWORD_BCRYPT, [ 'cost' => 11 ]); + } + if ($hash !== false && strpos($hash, '$') === 0) { + $node->password = $hash; + } else { + /* log and throw exception, not being able to hash the password should be fatal. */ + $this->getLogger('audit')->error(sprintf("Failed to hash password for user %s", $data['name'])); + throw new UserException(sprintf(gettext("Failed to hash password for user %s"), $data['name'])); + } + } + } + } + + public function searchAction() + { + $result = $this->searchBase('user'); + if (!empty($result['rows'])){ + /* XXX: this is a bit of a gimmick, for performance reasons we might decide to drop this at some point */ + foreach ($result['rows'] as &$row) { + $row['is_admin'] = in_array('page-all', $this->getModel()->getUserPrivs($row['name'])) ? '1' : '0'; + /* shells usually start with a /, prevent default text and translations triggering the warning */ + $row['shell_warning'] = strpos($row['shell'], '/') === 0 && empty($row['is_admin']) ? '1' :'0'; + } + } + return $result; + } + + public function getAction($uuid = null) + { + $result = $this->getBase('user', 'user', $uuid); + $result['user']['otp_uri'] = ''; + if (!empty($result['user']['otp_seed'])) { + $result['user']['otp_uri'] = sprintf( + "otpauth://totp/%s@%s?secret=%s&issuer=OPNsense&image=https://docs.opnsense.org/_static/favicon.png", + $result['user']['name'], + $this->getHostname(), + $result['user']['otp_seed'] + ); + } + if ((new \OPNsense\Core\ACL())->isPageAccessible($_SESSION['Username'], '/api/trust/cert')) { + $result['user']['certs'] = []; + } + return $result; + } + + public function newOtpSeedAction() + { + $seed = \Base32\Base32::encode(random_bytes(20)); + return [ + 'seed' => $seed, + 'otp_uri_template' => sprintf( + "otpauth://totp/%s@%s?secret=%s&issuer=OPNsense&image=https://docs.opnsense.org/_static/favicon.png", + '|USER|', + $this->getHostname(), + $seed + ) + ]; + } + + public function addAction() + { + $result = $this->addBase('user', 'user'); + if ($result['result'] != 'failed') { + $data = $this->request->getPost(static::$internalModelName); + if (!empty($data['name'])) { + (new Backend())->configdRun('auth sync user '. $data['name']); + } + } + return $result; + } + + public function setAction($uuid = null) + { + $result = $this->setBase('user', 'user', $uuid); + if ($result['result'] != 'failed') { + $data = $this->request->getPost(static::$internalModelName); + if (!empty($data['name'])) { + (new Backend())->configdRun('auth sync user '. $data['name']); + } + } + return $result; + } + + public function delAction($uuid) + { + $username = null; + if ($this->request->isPost()) { + Config::getInstance()->lock(); + $node = $this->getModel()->getNodeByReference('user.' . $uuid); + if ($node->scope == 'system') { + throw new UserException(sprintf(gettext("Not allowed to delete system user %s"), $node->name)); + } + if (!empty($node)) { + $username = (string)$node->name; + } + } + $result = $this->delBase('user', $uuid); + if ($username != null) { + (new Backend())->configdRun('auth sync user '. $username); + } + return $result; + } + + public function searchApiKeyAction() + { + return $this->searchRecordsetBase($this->getModel()->getApiKeys()); + } + + public function delApiKeyAction($id) + { + /* id is a base64 encoded string, we need to encode 'key' to prevent mangling data in the request */ + $key = base64_decode($id); + if ($key !== null && $this->request->isPost()) { + Config::getInstance()->lock(); + $user = $this->getModel()->getUserByApiKey($key); + if ($user !== null) { + $user->apikeys->del($key); + $this->save(false, true); + return ['result' => 'deleted']; + } else { + return ['result' => 'not found']; + } + } + return ["result" => "failed"]; + } + + public function addApiKeyAction($username) + { + if ($this->request->isPost()) { + Config::getInstance()->lock(); + $user = $this->getModel()->getUserByName($username); + if ($user != null) { + $tmp = $user->apikeys->add(); + if (!empty($tmp)) { + $this->save(false, true); + return array_merge(['result' => 'ok', 'hostname' => $this->getHostname()], $tmp); + } + } + Config::getInstance()->unlock(); + } + return ["result" => "failed"]; + } + +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/GroupController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/GroupController.php new file mode 100644 index 00000000000..e36e200507e --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/GroupController.php @@ -0,0 +1,39 @@ +view->formDialogEditGroup = $this->getForm("dialogGroup"); + $this->view->pick('OPNsense/Auth/group'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/PrivController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/PrivController.php new file mode 100644 index 00000000000..22bc480cc23 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/PrivController.php @@ -0,0 +1,39 @@ +view->formDialogEditPriv = $this->getForm("dialogPriv"); + $this->view->pick('OPNsense/Auth/priv'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/UserController.php b/src/opnsense/mvc/app/controllers/OPNsense/Auth/UserController.php new file mode 100644 index 00000000000..70e0cbb4bf5 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/UserController.php @@ -0,0 +1,56 @@ +view->formDialogEditUser = $this->getForm("dialogUser"); + $this->view->pick('OPNsense/Auth/user'); + } +} diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogGroup.xml b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogGroup.xml new file mode 100644 index 00000000000..29ff207965d --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogGroup.xml @@ -0,0 +1,34 @@ +
+ + group.scope + + info + + + group.gid + + info + + + group.name + + text + + + group.description + + text + You may enter a description here for your reference (not parsed). + + + group.priv + + select_multiple + + + group.member + + select_multiple + List of users that are a member of this group + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogPriv.xml b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogPriv.xml new file mode 100644 index 00000000000..b63a99a7c8f --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogPriv.xml @@ -0,0 +1,17 @@ +
+ + priv.id + + info + + + priv.users + + select_multiple + + + priv.groups + + select_multiple + +
diff --git a/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogUser.xml b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogUser.xml new file mode 100644 index 00000000000..0f0b883efe0 --- /dev/null +++ b/src/opnsense/mvc/app/controllers/OPNsense/Auth/forms/dialogUser.xml @@ -0,0 +1,101 @@ +
+ + user.scope + + info + + + user.uid + + info + + + user.disabled + + checkbox + Deny authentication, only applicable for local users + + + user.name + + text + + + user.password + + password + + + user.scrambled_password + + checkbox + Generate a scrambled password to prevent local database logins for this user. + + + user.descr + + text + User's full name, for your own information only + + + user.email + + text + User's e-mail address, for your own information only + + + user.comment + + textbox + User comment, for your own information only + + + user.landing_page + + text + Preferred landing page after login or authentication failure + + + user.language + + dropdown + + + user.shell + + dropdown + + + user.expires + + text + + + + user.group_memberships + + select_multiple + + + user.priv + + select_multiple + + + user.certs + + info + Client certificates matching this username by common name + + + user.otp_seed + + text + + + + user.authorizedkeys + + textbox + +
diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/API.php b/src/opnsense/mvc/app/library/OPNsense/Auth/API.php index 4cebd9463f6..bdd238cb5a8 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Auth/API.php +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/API.php @@ -31,6 +31,7 @@ namespace OPNsense\Auth; use OPNsense\Core\Config; +use OPNsense\Auth\User; /** * Class API key/secret database connector (connect to legacy xml structure). @@ -41,7 +42,7 @@ class API extends Base implements IAuthConnector /** * @var array internal list of authentication properties */ - private $lastAuthProperties = array(); + private $lastAuthProperties = []; /** * type name in configuration @@ -70,64 +71,6 @@ public function getLastAuthProperties() return $this->lastAuthProperties; } - - /** - * generate a new api key for an existing user - * @param $username username - * @return array|null apikey/secret pair - */ - public function createKey($username) - { - $configObj = Config::getInstance()->object(); - foreach ($configObj->system->children() as $key => $value) { - if ($key == 'user' && (string)$username == (string)$value->name) { - if (!isset($value->apikeys)) { - $apikeys = $value->addChild('apikeys'); - } else { - $apikeys = $value->apikeys; - } - $item = $apikeys->addChild('item'); - - $newKey = base64_encode(random_bytes(60)); - $newSecret = base64_encode(random_bytes(60)); - - $item->addChild('key', $newKey); - $item->addChild('secret', crypt($newSecret, '$6$')); - Config::getInstance()->save(); - $response = array('key' => $newKey, 'secret' => $newSecret); - return $response; - } - } - return null; - } - - /** - * remove user api key - * @param string $username username - * @param string $apikey api key - * @return bool key found - */ - public function dropKey($username, $apikey) - { - $configObj = Config::getInstance()->object(); - foreach ($configObj->system->children() as $key => $value) { - if ($key == 'user' && (string)$username == (string)$value->name) { - if (isset($value->apikeys)) { - $indx = 0; - foreach ($value->apikeys->children() as $apiNodeId => $apiNode) { - if ($apiNodeId == 'item' && (string)$apiNode->key == $apikey) { - unset($value->apikeys->item[$indx]); - Config::getInstance()->save(); - return true; - } - $indx++; - } - } - } - } - return false; - } - /** * authenticate user against local database (in config.xml) * @param string $username username to authenticate @@ -137,47 +80,74 @@ public function dropKey($username, $apikey) public function _authenticate($username, $password) { // reset auth properties - $this->lastAuthProperties = array(); + $this->lastAuthProperties = []; // search local user in database - $configObj = Config::getInstance()->object(); - $userObject = null; - $apiKey = null; - $apiSecret = null; - foreach ($configObj->system->children() as $key => $value) { - if ($key == 'user') { - if (!empty($value->apikeys)) { - foreach ($value->apikeys->children() as $apikey) { - if (!empty($apikey->key) && (string)$apikey->key == $username) { - // api key found, stop search - $userObject = $value; - $apiSecret = (string)$apikey->secret; - break; - } - } - } - } - } + $userinfo = (new User())->getApiKeySecret($username); - if ($userObject != null) { - if (!empty((string)$userObject->disabled)) { + if ($userinfo != null) { + if (!empty($userinfo['disabled'])) { // disabled user return false; } if ( - !empty($userObject->expires) - && strtotime("-1 day") > strtotime(date("m/d/Y", strtotime((string)$userObject->expires))) + !empty($userinfo['expires']) + && strtotime("-1 day") > strtotime(date("m/d/Y", strtotime($userinfo['expires']))) ) { // expired user return false; } - if (password_verify($password, $apiSecret)) { + if (password_verify($password, $userinfo['secret'])) { // password ok, return successfully authentication - $this->lastAuthProperties['username'] = (string)$userObject->name; + $this->lastAuthProperties['username'] = $userinfo['name']; return true; } } return false; } + + /** + * generate a new api key for an existing user, backwards compatibility stub + * @param $username username + * @return array|null apikey/secret pair + */ + public function createKey($username) + { + Config::getInstance()->lock(); + $mdl = new \OPNsense\Auth\User(); + $user = $mdl->getUserByName($username); + if ($user) { + $tmp = $user->apikeys->add(); + if (!empty($tmp)) { + $mdl->serializeToConfig(false, true); + Config::getInstance()->save(); + return $tmp; + } + } + Config::getInstance()->unlock(); + return false; + } + + /** + * remove user api key, backwards compatibility stub + * @param string $username username + * @param string $apikey api key + * @return bool key found + */ + public function dropKey($username, $apikey) + { + Config::getInstance()->lock(); + $mdl = new \OPNsense\Auth\User(); + $user = $mdl->getUserByName($username); + if ($user) { + if ($user->apikeys->del($apikey)) { + $mdl->serializeToConfig(false, true); + Config::getInstance()->save(); + return true; + } + } + Config::getInstance()->unlock(); + return false; + } } diff --git a/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php b/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php index 23750089a1e..2d3d5bd578c 100644 --- a/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php +++ b/src/opnsense/mvc/app/library/OPNsense/Auth/Base.php @@ -47,7 +47,7 @@ abstract class Base /** * @var array internal list of LDAP errors */ - protected $lastAuthErrors = array(); + protected $lastAuthErrors = []; /** * return group memberships @@ -56,7 +56,7 @@ abstract class Base */ private function groups($username) { - $groups = array(); + $groups = []; $user = $this->getUser($username); if ($user != null) { $uid = (string)$user->uid; @@ -86,7 +86,7 @@ private function groups($username) */ public function checkPolicy($username, $old_password, $new_password) { - return array(); + return []; } /** @@ -182,7 +182,7 @@ protected function setGroupMembership($username, $memberof, $scope = [], $create // update when changed if ($user == null && $createuser) { // user creation when enabled - $add_user = json_decode((new Backend())->configdpRun("auth add user", array($username)), true); + $add_user = json_decode((new Backend())->configdpRun("auth add user",[$username]), true); if (!empty($add_user) && $add_user['status'] == 'ok') { Config::getInstance()->forceReload(); $user = $this->getUser($username); @@ -197,32 +197,31 @@ protected function setGroupMembership($username, $memberof, $scope = [], $create foreach ($cnf->system->group as $group) { $lc_groupname = strtolower((string)$group->name); if (in_array($lc_groupname, $sync_groups)) { - if ( - in_array((string)$user->uid, (array)$group->member) - && empty($ldap_groups[$lc_groupname]) - ) { - unset($group->member[array_search((string)$user->uid, (array)$group->member)]); + $members = []; + foreach ($group->member as $member) { + $members = array_merge($members, explode(',', $member)); + } + if (in_array((string)$user->uid, $members) && empty($ldap_groups[$lc_groupname])) { + unset($members[array_search((string)$user->uid, $members)]); + $group->member = implode(',', $members); syslog(LOG_NOTICE, sprintf( 'User: policy change for %s unlink group %s', $username, (string)$group->name )); - } elseif ( - !in_array((string)$user->uid, (array)$group->member) - && !empty($ldap_groups[$lc_groupname]) - ) { + } elseif (!in_array((string)$user->uid, $members) && !empty($ldap_groups[$lc_groupname])) { syslog(LOG_NOTICE, sprintf( 'User: policy change for %s link group %s [%s]', $username, (string)$group->name, $ldap_groups[$lc_groupname] )); - $group->addChild('member', (string)$user->uid); + $group->member = implode(',', array_merge($members, [(string)$user->uid])); } } } Config::getInstance()->save(); - (new Backend())->configdpRun("auth user changed", array($username)); + (new Backend())->configdpRun("auth user changed",[$username]); } } diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ApiKeyField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ApiKeyField.php new file mode 100644 index 00000000000..07be7ab9894 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ApiKeyField.php @@ -0,0 +1,139 @@ +item)) { + /* auto convert to simple text blob */ + $tmp = []; + foreach ($value->children() as $child) { + $tmp[] = sprintf("%s|%s", $child->key, $child->secret); + } + return parent::setValue(implode("\n", $tmp)); + } elseif (!empty($value)) { + /* update only */ + return parent::setValue($value); + } + } + + /** + * get api key + secret + * @param string $key + * @return array with key and crypted secret + */ + public function get(string $key) + { + foreach (array_filter(explode("\n", $this->getCurrentValue())) as $line) { + $parts = explode("|", $line); + if (count($parts) == 2 && $parts[0] == $key) { + return [ + "key" => $parts[0], + "secret" => $parts[1] + ]; + } + } + return null; + } + + /** + * @return array list of keys (without secrets) + */ + public function all() + { + $result = []; + foreach (array_filter(explode("\n", $this->getCurrentValue())) as $line) { + $parts = explode("|", $line); + if (count($parts) == 2) { + $result[] = ['key' => $parts[0], 'id' => base64_encode($parts[0])]; + } + } + return $result; + } + + /** + * remove api key + * @param string $key + * @return bool key found + */ + public function del(string $key) + { + $found = false; + $tmp = ''; + $searchkey = sprintf("%s|", $key); + foreach (array_filter(explode("\n", $this->getCurrentValue())) as $line) { + if (strpos($line, $searchkey) === 0) { + $found = true; + } else { + $tmp .= sprintf("%s\n", $line); + } + } + $this->internalValue = $tmp; + return $found; + } + + /** + * generate a new key and return it + * @return array generated key+secret + */ + public function add() + { + $result = [ + 'key' => base64_encode(random_bytes(60)), + 'secret' => base64_encode(random_bytes(60)) + ]; + $new_items = array_merge( + array_filter(explode("\n", $this->getCurrentValue())), + [sprintf("%s|%s", $result['key'], crypt($result['secret'], '$6$'))] + ); + + $this->internalValue = implode("\n", $new_items); + return $result; + } +} + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ExpiresField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ExpiresField.php new file mode 100644 index 00000000000..30140b3639b --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/ExpiresField.php @@ -0,0 +1,82 @@ +format("m/d/Y")); + } catch (\Exception $ex) { + parent::setValue($value); + } + } else { + parent::setValue($value); + } + } + + /** + * retrieve field validators for this field type + * @return array returns Text/regex validator + */ + public function getValidators() + { + $validators = parent::getValidators(); + if ($this->internalValue != null) { + $validators[] = new CallbackValidator(["callback" => function ($data) { + /* + * Check for a valid expirationdate if one is set at all (valid means, + * DateTime puts out a time stamp so any DateTime compatible time + * format may be used. to keep it simple for the enduser, we only + * claim to accept MM/DD/YYYY as inputs. Advanced users may use inputs + * like "+1 day", which will be converted to MM/DD/YYYY based on "now". + * Otherwise such an entry would lead to an invalid expiration data. + */ + try { + (new \DateTime($data))->format("m/d/Y"); + } catch (\Exception $ex) { + return [gettext("Invalid expiration date format; use MM/DD/YYYY instead.")]; + } + } + ]); + } + return $validators; + } +} + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GidField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GidField.php new file mode 100644 index 00000000000..9f4c5b9a3f1 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GidField.php @@ -0,0 +1,111 @@ +fieldLoaded) { + $gids = []; + foreach ($this->getParentModel()->group->iterateItems() as $group) { + $gids[] = (int)$group->gid->getCurrentValue(); + } + for ($i=2000; true; $i++) { + if (!in_array($i, $gids)) { + parent::setValue((string)$i); + break; + } + } + } elseif (empty((string)$this)) { + parent::setValue($value); + } + } + + /** + * {@inheritdoc} + */ + protected function actionPostLoadingEvent() + { + parent::actionPostLoadingEvent(); + $this->fieldLoaded = true; + } + + /** + * {@inheritdoc} + */ + public function applyDefault() + { + /** When cloned (add), set our default to a new sequence */ + $this->fieldLoaded = true; + $this->setValue(null); + } + + + /** + * retrieve field validators for this field type + * @return array returns Text/regex validator + */ + public function getValidators() + { + $validators = parent::getValidators(); + + if ($this->internalValue != null) { + $validators[] = new IntegerValidator(['message' => $this->getValidationMessage()]); + $validators[] = new MinMaxValidator([ + 'message' => $this->getValidationMessage(), + 'min' => 0, + 'max' => 65535, + ]); + } + return $validators; + } +} + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GroupMembershipField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GroupMembershipField.php new file mode 100644 index 00000000000..d7ee0490165 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/GroupMembershipField.php @@ -0,0 +1,65 @@ +getParentNode()->uid; + if (self::$groups === null) { + self::$groups = []; + self::$memberList = []; + foreach (Config::getInstance()->object()->system->children() as $tag => $node) { + if ($tag == 'group') { + self::$groups[(string)$node->gid] = (string)$node->name; + foreach ($node->member as $value) { + foreach (explode(',', $value) as $uid) { + if (!isset(self::$memberList[$uid])) { + self::$memberList[$uid] = []; + } + self::$memberList[$uid][] = (string)$node->gid; + } + } + } + } + } + $this->internalOptionList = self::$groups; + if (isset(self::$memberList[$this_uid])) { + $this->internalValue = implode(',', self::$memberList[$this_uid]); + } + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/MemberField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/MemberField.php new file mode 100644 index 00000000000..e09f002861a --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/MemberField.php @@ -0,0 +1,63 @@ +user->iterateItems() as $node) { + self::$uid_list[(string)$node->uid] = (string)$node->name; + } + + } + $this->internalOptionList = self::$uid_list; + } + + /** + * {@inheritdoc} + */ + public function setValue($value) + { + if (is_a($value, 'SimpleXMLElement') && $value->count() > 1) { + return parent::setValue(implode(',', (array)$value)); + } else { + return parent::setValue($value); + } + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/PrivField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/PrivField.php new file mode 100644 index 00000000000..efeb53bf73f --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/PrivField.php @@ -0,0 +1,60 @@ +getPrivList() as $aclKey => $priv) { + self::$priv_list[$aclKey] = $priv['name']; + } + } + $this->internalOptionList = self::$priv_list; + } + + /** + * {@inheritdoc} + */ + public function setValue($value) + { + if (is_a($value, 'SimpleXMLElement') && $value->count() > 1) { + return parent::setValue(implode(',', (array)$value)); + } else { + return parent::setValue($value); + } + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/StoreB64Field.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/StoreB64Field.php new file mode 100644 index 00000000000..996642cbd33 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/StoreB64Field.php @@ -0,0 +1,57 @@ +getCurrentValue()) ?? ''; + } + + /** + * {@inheritdoc} + */ + public function setValue($value) + { + if (is_a($value, 'SimpleXMLElement')) { + return parent::setValue($value); + } elseif (!empty($value)) { + /* store value as base64 */ + return parent::setValue(base64_encode($value)); + } + } +} + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/UidField.php b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/UidField.php new file mode 100644 index 00000000000..bc3f6b86946 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/FieldTypes/UidField.php @@ -0,0 +1,111 @@ +fieldLoaded) { + $uids = []; + foreach ($this->getParentModel()->user->iterateItems() as $user) { + $uids[] = (int)$user->uid->getCurrentValue(); + } + for ($i=2000; true; $i++) { + if (!in_array($i, $uids)) { + parent::setValue((string)$i); + break; + } + } + } elseif (empty((string)$this)) { + parent::setValue($value); + } + } + + /** + * {@inheritdoc} + */ + protected function actionPostLoadingEvent() + { + parent::actionPostLoadingEvent(); + $this->fieldLoaded = true; + } + + /** + * {@inheritdoc} + */ + public function applyDefault() + { + /** When cloned (add), set our default to a new sequence */ + $this->fieldLoaded = true; + $this->setValue(null); + } + + + /** + * retrieve field validators for this field type + * @return array returns Text/regex validator + */ + public function getValidators() + { + $validators = parent::getValidators(); + + if ($this->internalValue != null) { + $validators[] = new IntegerValidator(['message' => $this->getValidationMessage()]); + $validators[] = new MinMaxValidator([ + 'message' => $this->getValidationMessage(), + 'min' => 0, + 'max' => 65535, + ]); + } + return $validators; + } +} + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/Group.php b/src/opnsense/mvc/app/models/OPNsense/Auth/Group.php new file mode 100644 index 00000000000..3fcc174597a --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/Group.php @@ -0,0 +1,56 @@ +group->iterateItems() as $node) { + if (!$validateFullModel && !$node->isFieldChanged()) { + continue; + } + $key = $node->__reference; + /* XXX: validate reserved groups? (/etc/groups)*/ + } + return $messages; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/Group.xml b/src/opnsense/mvc/app/models/OPNsense/Auth/Group.xml new file mode 100644 index 00000000000..a8e747dafd4 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/Group.xml @@ -0,0 +1,31 @@ + + /system/group+ + 1.0.0 + + + + + /^[a-zA-Z0-9\.\-_]{1,32}$/ + A groupname must contain a maximum of 32 alfanumeric characters + Y + + + UniqueConstraint + This groupname already exist. + + + + + user + Y + + + + Y + + + Y + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/Priv.php b/src/opnsense/mvc/app/models/OPNsense/Auth/Priv.php new file mode 100644 index 00000000000..2534d81b48c --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/Priv.php @@ -0,0 +1,39 @@ + + :memory: + 1.0.0 + + + + + OPNsense.Auth.User + user + name + + + Y + + + + + OPNsense.Auth.Group + group + name + + + Y + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/User.php b/src/opnsense/mvc/app/models/OPNsense/Auth/User.php new file mode 100644 index 00000000000..50b85a4a59b --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/User.php @@ -0,0 +1,151 @@ +user->iterateItems() as $node) { + if ($node->name == $name) { + return $node; + } + } + return null; + } + + /** + * @param string $name username + * @return User object + */ + public function getUserByApiKey(string $key) + { + foreach ($this->user->iterateItems() as $node) { + if ($node->apikeys->get($key) !== null) { + return $node; + } + } + return null; + } + + /** + * @param string $key api key + * @return array authentication data + */ + public function getApiKeySecret(string $key) + { + foreach ($this->user->iterateItems() as $node) { + $item = $node->apikeys->get($key); + if (!empty($item)) { + $item['name'] = (string)$node->name; + $item['disabled'] = (string)$node->disabled; + $item['expires'] = (string)$node->expires; + return $item; + } + } + return null; + } + + /** + * @return array list of api key records + */ + public function getApiKeys() + { + $result = []; + foreach ($this->user->iterateItems() as $node) { + foreach ($node->apikeys->all() as $apikey) { + $result[] = array_merge(['username' => (string)$node->name], $apikey); + } + } + return $result; + } + + /** + * @param string username + * @return array list of privileges for this user + */ + public function getUserPrivs($username) + { + $result = []; + foreach ($this->user->iterateItems() as $node) { + if ($node->name == $username) { + $result = array_merge($result, explode(',', $node->priv)); + $all_groups = explode(',', $node->group_memberships); + if (empty($all_groups)) { + break; + } + foreach (Config::getInstance()->object()->system->group as $node) { + if (in_array($node->gid, $all_groups)) { + foreach ($node->priv as $value) { + foreach (explode(',', $value) as $priv) { + if (!in_array($priv, $result)) { + $result[] = $priv; + } + } + } + } + } + break; + } + } + return $result; + } + + /** + * {@inheritdoc} + */ + public function performValidation($validateFullModel = false) + { + $messages = parent::performValidation($validateFullModel); + + foreach ($this->user->iterateItems() as $node) { + if (!$validateFullModel && !$node->isFieldChanged()) { + continue; + } + $key = $node->__reference; + if (empty((string)$node->password->getCurrentValue()) && empty((string)$node->scrambled_password)) { + $messages->appendMessage(new Message(gettext("A password is required"), $key . ".password")); + } + /* XXX: validate reserved users? (/etc/passwd)*/ + } + return $messages; + } +} diff --git a/src/opnsense/mvc/app/models/OPNsense/Auth/User.xml b/src/opnsense/mvc/app/models/OPNsense/Auth/User.xml new file mode 100644 index 00000000000..ba4548ad5c4 --- /dev/null +++ b/src/opnsense/mvc/app/models/OPNsense/Auth/User.xml @@ -0,0 +1,57 @@ + + /system/user+ + 1.0.0 + + + + + /^[a-zA-Z0-9\.\-_]{1,32}$/ + A username must contain a maximum of 32 alfanumeric characters + Y + + + UniqueConstraint + This username already exist. + + + + + 0 + Y + + + user + Y + + + + + + Default (none for all but root) + + /bin/csh + /bin/sh + /bin/tcsh + + + + + + + + + + + Y + + + system list locales + Default + + + Y + + + + + diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml index 7206f47ea56..0dde0635046 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/ACL/ACL.xml @@ -603,15 +603,17 @@ System: Group manager - system_groupmanager.php* + ui/auth/group + api/auth/group/* - - System: Group Manager: Add Privileges + + System: Access: Privileges - system_usermanager_addprivs.php?group* + ui/auth/priv + api/auth/priv/* - + System: High Availability @@ -635,16 +637,10 @@ System: User Manager - system_usermanager.php* - system_usermanager_import_ldap.php* + ui/auth/user + api/auth/user/* - - System: User Manager: Add Privileges - - system_usermanager_addprivs.php?user* - - Lobby: Password diff --git a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml index 8fdf796c88b..4f90ee5080d 100644 --- a/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml +++ b/src/opnsense/mvc/app/models/OPNsense/Core/Menu/Menu.xml @@ -18,14 +18,9 @@ - - - - - - - - + + + diff --git a/src/opnsense/mvc/app/views/OPNsense/Auth/group.volt b/src/opnsense/mvc/app/views/OPNsense/Auth/group.volt new file mode 100644 index 00000000000..c820a1e8791 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Auth/group.volt @@ -0,0 +1,87 @@ +{# + # Copyright (c) 2024 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + +
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Member Count') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogEditGroup,'id':'DialogGroup','label':lang._('Edit Group')])}} diff --git a/src/opnsense/mvc/app/views/OPNsense/Auth/priv.volt b/src/opnsense/mvc/app/views/OPNsense/Auth/priv.volt new file mode 100644 index 00000000000..f472ea80190 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Auth/priv.volt @@ -0,0 +1,84 @@ +{# + # Copyright (c) 2024 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + +
+
+ + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Match') }}{{ lang._('Users') }}{{ lang._('Groups') }}{{ lang._('Commands') }}
+
+
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogEditPriv,'id':'DialogPriv','label':lang._('Edit Privilege')])}} diff --git a/src/opnsense/mvc/app/views/OPNsense/Auth/user.volt b/src/opnsense/mvc/app/views/OPNsense/Auth/user.volt new file mode 100644 index 00000000000..83fe0b2da78 --- /dev/null +++ b/src/opnsense/mvc/app/views/OPNsense/Auth/user.volt @@ -0,0 +1,239 @@ +{# + # Copyright (c) 2024 Deciso B.V. + # All rights reserved. + # + # Redistribution and use in source and binary forms, with or without modification, + # are permitted provided that the following conditions are met: + # + # 1. Redistributions of source code must retain the above copyright notice, + # this list of conditions and the following disclaimer. + # + # 2. Redistributions in binary form must reproduce the above copyright notice, + # this list of conditions and the following disclaimer in the documentation + # and/or other materials provided with the distribution. + # + # THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + # INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + # AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + # AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + # OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + # POSSIBILITY OF SUCH DAMAGE. + #} + + + + + +
+
+ + + + + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Name') }}{{ lang._('Email') }}{{ lang._('Comments') }}{{ lang._('Language') }}{{ lang._('Groups') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+ + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Username') }}{{ lang._('Api key') }}{{ lang._('Commands') }}
+ +
+
+
+ +{{ partial("layout_partials/base_dialog",['fields':formDialogEditUser,'id':'DialogUser','label':lang._('Edit User')])}} diff --git a/src/opnsense/scripts/auth/sync_group.php b/src/opnsense/scripts/auth/sync_group.php new file mode 100755 index 00000000000..862e1c40b61 --- /dev/null +++ b/src/opnsense/scripts/auth/sync_group.php @@ -0,0 +1,80 @@ +#!/usr/local/bin/php + 65000) { + continue; + } + $localgroups[$line[0]] = $line; + } + } + + $update_group = null; + $groupdb = []; + foreach ($a_group as $groupent) { + $groupdb[] = $groupent['name']; + if ($groupent['name'] == $groupname) { + $update_group = $groupent; + } + } + + /* rename/delete situations */ + foreach ($localgroups as $item) { + if (!in_array($item[0], $groupdb)) { + mwexecf('/usr/sbin/pw groupdel -n %s', [$item[0]]); + } + } + + /* add or update when found */ + if ($update_group) { + local_group_set($update_group); + echo json_encode(["status" => "updated"]); + } else { + echo json_encode(["status" => "not_found"]); + } +} diff --git a/src/opnsense/scripts/auth/sync_user.php b/src/opnsense/scripts/auth/sync_user.php new file mode 100755 index 00000000000..b2019fd5609 --- /dev/null +++ b/src/opnsense/scripts/auth/sync_user.php @@ -0,0 +1,81 @@ +#!/usr/local/bin/php + 65000) { + continue; + } + $localusers[$line[0]] = $line; + } + } + + $update_user = null; + $userdb = []; + foreach ($a_user as $userent) { + $userdb[] = $userent['name']; + if ($userent['name'] == $username) { + $update_user = $userent; + } + } + + /* rename/delete situations */ + foreach ($localusers as $item) { + if (!in_array($item[0], $userdb)) { + mwexecf('/usr/sbin/pw userdel -n %s', [$item[0]]); + } + } + /* add or update when found */ + if ($update_user) { + local_user_set($update_user, false, $localusers[$username] ?? []); + /* signal backend that the user has changed. (update groups) */ + mwexecf('/usr/local/sbin/pluginctl -c user_changed '. $username); + echo json_encode(["status" => "updated"]); + } else { + echo json_encode(["status" => "not_found"]); + } +} diff --git a/src/opnsense/scripts/system/get_locales.php b/src/opnsense/scripts/system/get_locales.php new file mode 100755 index 00000000000..4907e9a70d0 --- /dev/null +++ b/src/opnsense/scripts/system/get_locales.php @@ -0,0 +1,33 @@ +#!/usr/local/bin/php + - * Copyright (C) 2005 Paul Taylor - * Copyright (C) 2003-2005 Manuel Kasper - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, - * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -require_once("guiconfig.inc"); - -$a_group = &config_read_array('system', 'group'); - -if ($_SERVER['REQUEST_METHOD'] === 'GET') { - if (isset($_GET['groupid']) && isset($a_group[$_GET['groupid']])) { - $id = $_GET['groupid']; - } - if (isset($_GET['act']) && ($_GET['act'] == 'edit' || $_GET['act'] == 'new')) { - $act = $_GET['act']; - } else { - $act = null; - } - $pconfig = array(); - if ($act == "edit" && isset($id)) { - // read config - $pconfig['name'] = $a_group[$id]['name']; - $pconfig['gid'] = $a_group[$id]['gid']; - $pconfig['scope'] = $a_group[$id]['scope']; - $pconfig['description'] = $a_group[$id]['description']; - $pconfig['members'] = isset($a_group[$id]['member']) ? $a_group[$id]['member'] : array(); - $pconfig['priv'] = isset($a_group[$id]['priv']) ? $a_group[$id]['priv'] : array(); - } elseif ($act != null) { - // init defaults - $pconfig['name'] = null; - $pconfig['gid'] = null; - $pconfig['scope'] = null; - $pconfig['description'] = null; - $pconfig['members'] = array(); - $pconfig['priv'] = array(); - } -} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - if (isset($a_group[$_POST['groupid']])) { - $id = $_POST['groupid']; - } - $pconfig = $_POST; - $input_errors = array(); - $act = (isset($pconfig['act']) ? $pconfig['act'] : ''); - - $user = getUserEntry($_SESSION['Username']); - $a_user = &config_read_array('system', 'user'); - if (userHasPrivilege($user, 'user-config-readonly')) { - $input_errors[] = gettext('You do not have the permission to perform this action.'); - } elseif (isset($id) && $act == "delgroup" && isset($pconfig['groupname']) && $pconfig['groupname'] == $a_group[$id]['name']) { - $prev_members = !empty($a_group[$id]['member']) ? $a_group[$id]['member'] : array(); - local_group_del($a_group[$id]); - unset($a_group[$id]); - write_config(); - // XXX: signal backend about changed users for the members of this group - foreach ($prev_members as $member) { - foreach ($a_user as & $user) { - if ($user['uid'] == $member) { - configdp_run('auth user changed', [$user['name']]); - } - } - } - header(url_safe('Location: /system_groupmanager.php')); - exit; - } elseif (isset($pconfig['save'])) { - $reqdfields = explode(" ", "name"); - $reqdfieldsn = array(gettext("Group Name")); - - do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); - - if (preg_match("/[^a-zA-Z0-9\.\-_]/", $pconfig['name'])) { - $input_errors[] = gettext("The group name contains invalid characters."); - } - - if (strlen($pconfig['name']) > 32) { - $input_errors[] = gettext("The group name is longer than 32 characters."); - } - - if (count($input_errors) == 0 && !isset($id)) { - /* make sure there are no dupes */ - foreach ($a_group as $group) { - if ($group['name'] == $pconfig['name']) { - $input_errors[] = gettext("Another entry with the same group name already exists."); - break; - } - } - - $sys_groups = file_get_contents('/etc/group'); - foreach (explode("\n", $sys_groups) as $line) { - if (explode(":", $line)[0] == $pconfig['name']) { - $input_errors[] = gettext("That groupname is reserved by the system."); - } - } - } - - if (count($input_errors) == 0) { - $group = array(); - if (isset($id) && $a_group[$id]) { - $group = $a_group[$id]; - } - $prev_members = !empty($group['member']) ? $group['member'] : array(); - - $group['name'] = $pconfig['name']; - $group['description'] = $pconfig['description']; - - if (empty($pconfig['members'])) { - unset($group['member']); - } else { - $group['member'] = $pconfig['members']; - } - - if (isset($id) && $a_group[$id]) { - $a_group[$id] = $group; - } else { - $group['gid'] = $config['system']['nextgid']++; - $a_group[] = $group; - } - - local_group_set($group); - - /* Refresh users in this group since their privileges may have changed. - XXX: it looks like local_user_set's intend is to only change group assignments, if that's - the case, it should be safe to drop the block below and let configd handle it. - */ - - if (is_array($group['member'])) { - foreach ($a_user as & $user) { - if (in_array($user['uid'], $group['member'])) { - local_user_set($user); - } - } - } - if (isset($id) && $a_group[$id]) { - $audit_msg = sprintf("group \"%s\" changed", $group['name']); - } else { - $audit_msg = sprintf("group \"%s\" created", $group['name']); - } - write_config($audit_msg); - // XXX: signal backend which users have changed. - // core_user_changed_groups() would change local group assignments in that case as well. - $new_members = !empty($group['member']) ? $group['member'] : array(); - $all_members = array_merge($prev_members, $new_members); - foreach ($all_members as $member) { - if (!in_array($member, $prev_members) || !in_array($member, $new_members)) { - foreach ($a_user as & $user) { - if ($user['uid'] == $member) { - configdp_run('auth user changed', [$user['name']]); - } - } - } - } - header(url_safe('Location: /system_groupmanager.php')); - exit; - } else { - // input errors, load page in edit mode - $act = 'edit'; - } - } else { - // POST without a valid action, redirect to overview - header(url_safe('Location: /system_groupmanager.php')); - exit; - } -} - -legacy_html_escape_form_data($pconfig); -legacy_html_escape_form_data($a_group); - -include("head.inc"); - -?> - - - - - - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- /> -
- - -
- - - - - - - - - - - - - - -
 
- - -
- "> - - -

- "> - - -
- -
- -
- - - - - - - - - - - - - - -
- -
- - - -
-
- - " onclick="window.location.href='/system_groupmanager.php'" /> - - - - -
-
- -
- - " /> - - - - - - - - - - - - $group): ?> - - - - - - - - - - - - -
- - - -
- - - - - - - - - -
- - - - - - - - - - -
-
-
- -
-
-
-
-
- - - * Copyright (C) 2005 Paul Taylor - * Copyright (C) 2003-2005 Manuel Kasper - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, - * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -require_once 'guiconfig.inc'; -require_once 'system.inc'; -require_once 'base32/Base32.php'; - -function cert_get_dates($str_crt, $decode = true) -{ - if ($decode) { - $str_crt = base64_decode($str_crt); - } - $crt_details = openssl_x509_parse($str_crt); - if ($crt_details['validFrom_time_t'] > 0) { - $start = date('r', $crt_details['validFrom_time_t']); - } - if ($crt_details['validTo_time_t'] > 0) { - $end = date('r', $crt_details['validTo_time_t']); - } - return array($start, $end); -} - -function get_user_privdesc(& $user) -{ - global $priv_list; - - $privs = array(); - - if (!isset($user['priv']) || !is_array($user['priv'])) { - $user_privs = array(); - } else { - $user_privs = $user['priv']; - } - - $names = local_user_get_groups($user); - - foreach ($names as $name) { - $group = getGroupEntry($name); - if (isset($group['priv']) && is_array($group['priv'])) { - foreach ($group['priv'] as $pname) { - if (in_array($pname, $user_privs)) { - continue; - } - if (empty($priv_list[$pname])) { - continue; - } - $priv = $priv_list[$pname]; - $priv['group'] = $group['name']; - $priv['id'] = $pname; - $privs[] = $priv; - } - } - } - - foreach ($user_privs as $pname) { - if (!empty($priv_list[$pname])) { - $priv_list[$pname]['id'] = $pname; - $privs[] = $priv_list[$pname]; - } - } - - legacy_html_escape_form_data($privs); - return $privs; -} - -$a_user = &config_read_array('system', 'user'); - -// reset errors and action -$act = null; -if ($_SERVER['REQUEST_METHOD'] === 'GET') { - // process get type actions - if (isset($_GET['userid']) && isset($a_user[$_GET['userid']])) { - $id = $_GET['userid']; - } - if (isset($_GET['act'])) { - $act = $_GET['act']; - } - if (isset($_GET['savemsg'])) { - $savemsg = htmlspecialchars($_GET['savemsg']); - } - if ($act == "expcert" && isset($id)) { - if (!(new OPNsense\Core\ACL())->isPageAccessible($_SESSION['Username'], '/api/trust/cert')) { - exit; - } - // export certificate - $cert = &lookup_cert($a_user[$id]['cert'][$_GET['certid']]); - - $exp_name = urlencode("{$a_user[$id]['name']}-{$cert['descr']}.crt"); - $exp_data = base64_decode($cert['crt']); - $exp_size = strlen($exp_data); - - header("Content-Type: application/octet-stream"); - header("Content-Disposition: attachment; filename={$exp_name}"); - header("Content-Length: $exp_size"); - echo $exp_data; - exit; - } elseif ($act == "expckey" && isset($id)) { - if (!(new OPNsense\Core\ACL())->isPageAccessible($_SESSION['Username'], '/api/trust/cert')) { - exit; - } - // export private key - $cert = &lookup_cert($a_user[$id]['cert'][$_GET['certid']]); - $exp_name = urlencode("{$a_user[$id]['name']}-{$cert['descr']}.key"); - $exp_data = base64_decode($cert['prv']); - $exp_size = strlen($exp_data); - - header("Content-Type: application/octet-stream"); - header("Content-Disposition: attachment; filename={$exp_name}"); - header("Content-Length: $exp_size"); - echo $exp_data; - exit; - } elseif ($act == 'new' || $act == 'edit') { - // edit user, load or init data - $fieldnames = array('user_dn', 'descr', 'expires', 'scope', 'uid', 'priv', - 'otp_seed', 'email', 'shell', 'comment', 'landing_page'); - if (isset($id)) { - if (isset($a_user[$id]['authorizedkeys'])) { - $pconfig['authorizedkeys'] = base64_decode($a_user[$id]['authorizedkeys']); - } - if (isset($a_user[$id]['name'])) { - $pconfig['usernamefld'] = $a_user[$id]['name']; - } - $pconfig['groups'] = local_user_get_groups($a_user[$id]); - $pconfig['disabled'] = !empty($a_user[$id]['disabled']); - foreach ($fieldnames as $fieldname) { - if (isset($a_user[$id][$fieldname])) { - $pconfig[$fieldname] = $a_user[$id][$fieldname]; - } else { - $pconfig[$fieldname] = null; - } - } - - foreach (get_locale_list() as $lcode => $ldesc) { - if ($a_user[$id]['language'] == $lcode) { - $pconfig['language'] = $ldesc; - break; - } - } - } else { - // set defaults - $pconfig['groups'] = null; - $pconfig['disabled'] = false; - $pconfig['scope'] = "user"; - $pconfig['usernamefld'] = null; - foreach ($fieldnames as $fieldname) { - if (!isset($pconfig[$fieldname])) { - $pconfig[$fieldname] = null; - } - } - } - } - if (empty($pconfig['language'])) { - $pconfig['language'] = gettext('Default'); - } -} elseif ($_SERVER['REQUEST_METHOD'] === 'POST') { - // process post type requests - if (isset($_POST['userid']) && isset($a_user[$_POST['userid']])) { - $id = $_POST['userid']; - } - if (isset($_POST['act'])) { - $act = $_POST['act']; - } - $pconfig = $_POST; - $input_errors = array(); - - $user = getUserEntry($_SESSION['Username']); - if (userHasPrivilege($user, 'user-config-readonly')) { - $input_errors[] = gettext('You do not have the permission to perform this action.'); - } elseif ($act == "deluser" && isset($id)) { - // drop user - if ($_SESSION['Username'] === $a_user[$id]['name']) { - $input_errors[] = gettext('You cannot delete yourself.'); - } else { - local_user_del($a_user[$id]); - $userdeleted = $a_user[$id]['name']; - unset($a_user[$id]); - write_config(sprintf('The user "%s" was successfully removed.', $userdeleted)); - $savemsg = sprintf(gettext('The user "%s" was successfully removed.'), $userdeleted); - header(url_safe('Location: /system_usermanager.php?savemsg=%s', array($savemsg))); - exit; - } - } elseif ($act == "newApiKey" && isset($id)) { - // every action is using the sequence of the user, to keep it understandable, we will use - // the same strategy here (although we need a username to work with) - // - // the client side is (jquery) generates the actual download file. - $username = $a_user[$id]['name']; - $authFactory = new \OPNsense\Auth\AuthenticationFactory(); - $authenticator = $authFactory->get("Local API"); - $keyData = $authenticator->createKey($username); - if ($keyData != null) { - echo json_encode($keyData); - } - exit; - } elseif ($act =='delApiKey' && isset($id)) { - $username = $a_user[$id]['name']; - if (!empty($pconfig['api_delete'])) { - $authFactory = new \OPNsense\Auth\AuthenticationFactory(); - $authenticator = $authFactory->get("Local API"); - $authenticator->dropKey($username, $pconfig['api_delete']); - $savemsg = sprintf(gettext('The API key "%s" was successfully removed.'), $pconfig['api_delete']); - } else { - $savemsg = gettext('No API key found'); - } - // redirect - header(url_safe('Location: /system_usermanager.php?savemsg=%s&act=edit&userid=%d', array($savemsg, $id))); - exit; - } elseif (isset($pconfig['save']) || isset($pconfig['save_close'])) { - $reqdfields = explode(' ', 'usernamefld'); - $reqdfieldsn = array(gettext('Username')); - - do_input_validation($pconfig, $reqdfields, $reqdfieldsn, $input_errors); - - if (preg_match("/[^a-zA-Z0-9\.\-_]/", $pconfig['usernamefld'])) { - $input_errors[] = gettext("The username contains invalid characters."); - } - - if (strlen($pconfig['usernamefld']) > 32) { - $input_errors[] = gettext("The username is longer than 32 characters."); - } - - if (!empty($pconfig['passwordfld1']) || !empty($pconfig['passwordfld2'])) { - if ($pconfig['passwordfld1'] != $pconfig['passwordfld2']) { - $input_errors[] = gettext('The passwords do not match.'); - } elseif (empty($pconfig['gen_new_password'])) { - // check against local password policy - $authenticator = get_authenticator(); - $input_errors = array_merge( - $input_errors, - $authenticator->checkPolicy($pconfig['usernamefld'], null, $pconfig['passwordfld1']) - ); - } else { - $input_errors[] = gettext('Cannot set random password due to explicit input.'); - } - } - - if (!empty($pconfig['disabled']) && $_SESSION['Username'] === $a_user[$id]['name']) { - $input_errors[] = gettext('You cannot disable yourself.'); - } - - if (isset($id)) { - $oldusername = $a_user[$id]['name']; - } else { - $oldusername = ''; - - if (empty($pconfig['passwordfld1']) && empty($pconfig['gen_new_password'])) { - $input_errors[] = gettext('A password is required.'); - } - } - - /* make sure this user name is unique */ - if (count($input_errors) == 0) { - foreach ($a_user as $userent) { - if ($userent['name'] == $pconfig['usernamefld'] && $oldusername != $pconfig['usernamefld']) { - $input_errors[] = gettext("Another entry with the same username already exists."); - break; - } - } - } - - /* also make sure it is not reserved */ - if (count($input_errors) == 0) { - $system_users = explode("\n", file_get_contents("/etc/passwd")); - foreach ($system_users as $s_user) { - $ent = explode(":", $s_user); - if ($ent[0] == $pconfig['usernamefld'] && $oldusername != $pconfig['usernamefld']) { - $input_errors[] = gettext("That username is reserved by the system."); - break; - } - } - } - - /* - * Check for a valid expirationdate if one is set at all (valid means, - * DateTime puts out a time stamp so any DateTime compatible time - * format may be used. to keep it simple for the enduser, we only - * claim to accept MM/DD/YYYY as inputs. Advanced users may use inputs - * like "+1 day", which will be converted to MM/DD/YYYY based on "now". - * Otherwise such an entry would lead to an invalid expiration data. - */ - if (!empty($pconfig['expires'])) { - try { - $expdate = new DateTime($pconfig['expires']); - //convert from any DateTime compatible date to MM/DD/YYYY - $pconfig['expires'] = $expdate->format("m/d/Y"); - } catch (Exception $ex) { - $input_errors[] = gettext("Invalid expiration date format; use MM/DD/YYYY instead."); - } - } - - if (!empty($pconfig['shell']) && !in_array($pconfig['shell'], auth_get_shells(isset($id) ? $a_user[$id]['uid'] : $config['system']['nextuid']))) { - $input_errors[] = gettext('Invalid login shell provided.'); - } - - if (!count($input_errors)) { - $userent = array(); - - if (isset($id)) { - $userent = $a_user[$id]; - /* the user name was modified */ - if ($pconfig['usernamefld'] != $pconfig['oldusername']) { - local_user_del($userent); - } - } - - /* the user password was modified */ - if (!empty($pconfig['passwordfld1'])) { - local_user_set_password($userent, $pconfig['passwordfld1']); - } elseif (!empty($pconfig['gen_new_password'])) { - local_user_set_password($userent); - } - - isset($pconfig['scope']) ? $userent['scope'] = $pconfig['scope'] : $userent['scope'] = "system"; - - $userent['name'] = $pconfig['usernamefld']; - $userent['descr'] = $pconfig['descr']; - $userent['expires'] = $pconfig['expires']; - $userent['authorizedkeys'] = base64_encode(trim($pconfig['authorizedkeys'])); - if (!empty($pconfig['gen_otp_seed'])) { - // generate 160bit base32 encoded secret - $userent['otp_seed'] = Base32\Base32::encode(random_bytes(20)); - } else { - $userent['otp_seed'] = trim($pconfig['otp_seed']); - } - - if (!empty($pconfig['disabled'])) { - $userent['disabled'] = true; - } elseif (isset($userent['disabled'])) { - unset($userent['disabled']); - } - - if (!empty($pconfig['email'])) { - $userent['email'] = $pconfig['email']; - } elseif (isset($userent['email'])) { - unset($userent['email']); - } - - if (!empty($pconfig['comment'])) { - $userent['comment'] = $pconfig['comment']; - } elseif (isset($userent['comment'])) { - unset($userent['comment']); - } - if (!empty($pconfig['landing_page'])) { - $userent['landing_page'] = $pconfig['landing_page']; - } elseif (isset($userent['landing_page'])) { - unset($userent['landing_page']); - } - - if (!empty($pconfig['shell'])) { - $userent['shell'] = $pconfig['shell']; - } elseif (isset($userent['shell'])) { - unset($userent['shell']); - } - - if (isset($id)) { - $a_user[$id] = $userent; - } else { - $userent['uid'] = $config['system']['nextuid']++; - $a_user[] = $userent; - } - - local_user_set_groups($userent, $pconfig['groups']); - local_user_set($userent); - if (isset($id)) { - $audit_msg = sprintf("user \"%s\" changed", $userent['name']); - } else { - $audit_msg = sprintf("user \"%s\" created", $userent['name']); - } - write_config($audit_msg); - // XXX: signal backend that the user has changed. - configdp_run('auth user changed', [$userent['name']]); - - if (!empty($pconfig['chkNewCert'])) { - header(url_safe('Location: /ui/trust/cert#new=' . $userent['name'])); - } elseif (isset($pconfig['save_close'])) { - header(url_safe('Location: /system_usermanager.php?savemsg=%s', array(get_std_save_message(true)))); - } else { - header(url_safe('Location: /system_usermanager.php?act=edit&userid=%d&savemsg=%s', array(isset($id) ? $id : count($a_user) - 1, get_std_save_message(true)))); - } - exit; - } - } else { - header(url_safe('Location: /system_usermanager.php')); - exit; - } -} - -legacy_html_escape_form_data($pconfig); -legacy_html_escape_form_data($a_user); - -include("head.inc"); - -?> - - - - - - - - - -
-
-
- - - - - -
-
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -isPageAccessible($_SESSION['Username'], '/api/trust/cert')):?> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - -
- - -
- /> - -
- /> - -
- -
-
- -

- /> - -
- /> - -
- - -
- - -
- - -
- - -
- -
- - -
- - - - - - - - - - - - - - -
 
- - -
- "> - - -

- "> - - -
- -
- -
- - - - - - - - - - - - - - - - -
- -
- " data-toggle="tooltip"> - - -
-
- - - - - - - - - - - - - - - - - - - - -
- ".gettext('Revoked').")" : "";?> - - - - "> - - - "> - - -
- " data-toggle="tooltip"> - - -
-
- - - - - - - - - - - - - - - - - - - - -
- - -
- - - - - - - -
- -
- -
- -
- - - - -
- - - - -
- -
  - - - - - - -
-
- -
- - - - - - - - - - - - - - $userent): ?> - - - - - - - - - - - - -
- - - - - - - -
- - - - - - - - - -
- - - - - - - - - - - - -
-
-
- -
-
-
-
-
- - - - - - -
-
-
- 0) print_input_errors($input_errors); ?> -
-
-
- - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - -
- - - " id="search"> -
-
- - - - - - - - - - - $pdata) { - $pnamesafe = !empty($pdata['name']) ? $pdata['name'] : $pname; - switch (substr($pname, 0, 5)) { - case 'page-': - $pdesc = gettext('GUI'); - break; - case 'user-': - $pdesc = gettext('User'); - break; - default: - $pdesc = gettext('N/A'); - break; - } - $search_phrase = $pdesc . ' ' . $pnamesafe; - if (!empty($pdata['match'])) { - $search_phrase .= implode(' ', $pdata['match']); - } - ?> - - - - - - - - -
- > - - - /
- -
-
- - - - - - - - - - - -
-
- - " onclick="history.back()" /> -
-
-
-
-
-
-
- diff --git a/src/www/system_usermanager_import_ldap.php b/src/www/system_usermanager_import_ldap.php deleted file mode 100644 index 6555b1d3c46..00000000000 --- a/src/www/system_usermanager_import_ldap.php +++ /dev/null @@ -1,197 +0,0 @@ - - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, - * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY - * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE - * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, - * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - */ - -require_once("guiconfig.inc"); -require_once("auth.inc"); - -function add_local_user($username, $userdn, $userfullname, $useremail) -{ - global $config; - - foreach ($config['system']['user'] as &$user) { - if ($user['name'] == $username && $user['name'] != 'root') { - // link local user to remote server by updating user_dn - $user['user_dn'] = $userdn; - // trash user password when linking to ldap, avoid accidental login - // using fall-back local password. User could still reset its - // local password, but only by choice. - local_user_set_password($user); - local_user_set($user); - return; - } - } - // new user, add - $new_user = array(); - $new_user['scope'] = 'user'; - $new_user['name'] = $username; - $new_user['user_dn'] = $userdn; - $new_user['descr'] = $userfullname; - $new_user['email'] = $useremail; - local_user_set_password($new_user); - $new_user['uid'] = $config['system']['nextuid']++; - $config['system']['user'][] = $new_user; - local_user_set($new_user); -} - -$ldap_is_connected = false; -$ldap_users = array(); -$ldap_server = array(); -$authName = null; -$exit_form = false; - -// XXX find first LDAP GUI auth server, better select later on -$servers = explode(',', $config['system']['webgui']['authmode']); -foreach ($servers as $server) { - $authcfg = auth_get_authserver($server); - if ($authcfg['type'] == 'ldap' || $authcfg['type'] == 'ldap-totp') { - $authName = $server; - $ldap_server = $authcfg; - if (strstr($ldap_server['ldap_urltype'], "Standard") || strstr($ldap_server['ldap_urltype'], "StartTLS")) { - $ldap_server['ldap_full_url'] = "ldap://"; - } else { - $ldap_server['ldap_full_url'] = "ldaps://"; - } - $ldap_server['ldap_full_url'] .= is_ipaddrv6($authcfg['host']) ? "[{$authcfg['host']}]" : $authcfg['host']; - if (!empty($ldap_server['ldap_port'])) { - $ldap_server['ldap_full_url'] .= ":{$authcfg['ldap_port']}"; - } - break; - } -} - -if ($authName !== null) { - // connect to ldap server - $authenticator = (new OPNsense\Auth\AuthenticationFactory())->get($authName); - // search ldap - $ldap_is_connected = $authenticator->connect( - $ldap_server['ldap_full_url'], $ldap_server['ldap_binddn'], $ldap_server['ldap_bindpw'] - ); - - if ($ldap_is_connected) { - // collect list of current ldap users from config - $confDNs = array(); - foreach ($config['system']['user'] as $confUser) { - if (!empty($confUser['user_dn'])) { - $confDNs[] = trim($confUser['user_dn']); - } - } - // search ldap - $result = $authenticator->searchUsers('*', $ldap_server['ldap_attr_user'], $ldap_server['ldap_extended_query']); - // actual form action, either save new accounts or list missing - if ($_SERVER['REQUEST_METHOD'] === 'POST') { - // create selected accounts - $exit_form = true; - if (isset($_POST['user_dn'])) { - $update_count = 0; - foreach ($result as $ldap_user ) { - foreach ($_POST['user_dn'] as $userDN) { - if ($userDN == $ldap_user['dn'] && !in_array($ldap_user['dn'], $confDNs)) { - // strip domain if it exists and cleanse ldap username to make sure it is a valid one for - // our system. - $username = explode('@', $ldap_user['name'])[0]; - $username = substr(preg_replace("/[^a-zA-Z0-9\.\-_]/", "", $username),0 ,32); - add_local_user($username , $ldap_user['dn'], $ldap_user['fullname'], $ldap_user['email']); - $update_count++; - } - } - } - if ($update_count > 0){ - // write config when changed - write_config(); - } - } - } else { - if (is_array($result)) { - // list all missing accounts - foreach ($result as $ldap_user ) { - if (!in_array($ldap_user['dn'], $confDNs)) { - $ldap_users[$ldap_user['name']] = $ldap_user['dn']; - } - } - ksort($ldap_users); - } - } - } -} - -include('head.inc'); - -?> - - - - - -

- - -
- - - - - - - - - $userDN): ?> - - - - - - -
- -
-
- - - - - - -