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 @@
+
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 @@
+
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 @@
+
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
+
+
+
+
+
+ user
+ name
+
+
+ Y
+
+
+
+
+
+ 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.
+ #}
+
+
+
+
+
+
+{{ 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");
-
-?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- * 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");
-
-?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- print_alert_box(gettext('The login shell for this non-admin user is not active for security reasons.'), 'warning'); ?>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0) print_input_errors($input_errors); ?>
-
-
-
-
-
-
-
-
-
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');
-
-?>
-
-
-
-
-
-
=gettext("Could not connect to the LDAP server. Please check your LDAP configuration.");?>