From 34aa934afcfa67fdf24c76b3b19da9486d44fe26 Mon Sep 17 00:00:00 2001 From: Marc Mettke Date: Sun, 7 Apr 2019 07:12:49 +0200 Subject: [PATCH] Adding local user managment * keys-sync user becomes a local and not an ldap user * local users are allowed to access site without 403 * LDAP can be dis/enabled in the configuration * Added section to delete local user * Added section to add local users * Added graceful handling of deleted users in logs, nodes, ... There are tables containing NOT NULL fields for actors like the log table, which contains who did something. When we remove a user, we don't want to remove that log as the entity it is for is still alive. We instead want to remove the actor (or set it to null) so that the datbase stays consitent. The migrations in this patch recreate every table which has an actor to allow NULL fields. It also corrects a few constraints to set those values to NULL on delete. --- config/config-sample.ini | 1 + core.php | 10 ++- migrations/004.php | 159 +++++++++++++++++++++++++++++++++++ model/migrationdirectory.php | 2 +- model/user.php | 17 ++++ model/userdirectory.php | 41 ++++++--- requesthandler.php | 21 +++-- scripts/ldap_update.php | 22 +++-- scripts/sync.php | 1 + templates/functions.php | 4 + templates/group.php | 2 +- templates/server.php | 2 +- templates/user.php | 11 +++ templates/users.php | 74 ++++++++++++---- views/user.php | 9 +- views/users.php | 40 ++++++++- 16 files changed, 360 insertions(+), 56 deletions(-) create mode 100644 migrations/004.php diff --git a/config/config-sample.ini b/config/config-sample.ini index 030e70e..0b92cb7 100644 --- a/config/config-sample.ini +++ b/config/config-sample.ini @@ -83,6 +83,7 @@ password = password database = ska-db [ldap] +enabled = 0 ; Address to connect to LDAP server host = ldaps://ldap.example.com:636 ; Use StartTLS for connection security (recommended if using ldap:// instead diff --git a/core.php b/core.php index 618c15b..45acbb5 100644 --- a/core.php +++ b/core.php @@ -35,10 +35,12 @@ require('ldap.php'); require('email.php'); -$ldap_options = array(); -$ldap_options[LDAP_OPT_PROTOCOL_VERSION] = 3; -$ldap_options[LDAP_OPT_REFERRALS] = !empty($config['ldap']['follow_referrals']); -$ldap = new LDAP($config['ldap']['host'], $config['ldap']['starttls'], $config['ldap']['bind_dn'], $config['ldap']['bind_password'], $ldap_options); +if ($config['ldap']['enabled'] == 1) { + $ldap_options = array(); + $ldap_options[LDAP_OPT_PROTOCOL_VERSION] = 3; + $ldap_options[LDAP_OPT_REFERRALS] = !empty($config['ldap']['follow_referrals']); + $ldap = new LDAP($config['ldap']['host'], $config['ldap']['starttls'], $config['ldap']['bind_dn'], $config['ldap']['bind_password'], $ldap_options); +} setup_database(); $relative_frontend_base_url = (string)parse_url($config['web']['baseurl'], PHP_URL_PATH); diff --git a/migrations/004.php b/migrations/004.php new file mode 100644 index 0000000..9f088b7 --- /dev/null +++ b/migrations/004.php @@ -0,0 +1,159 @@ +store_result()) { + $res->free(); + } + } while ($database->more_results() && $database->next_result()); +} + +$this->database->autocommit(FALSE); + +$result = $this->database->query(" + SELECT uid FROM user WHERE uid = 'keys-sync' +"); +if ($result) { + if($result->num_rows === 0) { + $result->close(); + $result = $this->database->multi_query(" + INSERT INTO entity SET type = 'user'; + INSERT INTO user SET entity_id = ( + SELECT LAST_INSERT_ID() + ), uid = 'keys-sync', name = 'Synchronization script', email = '', auth_realm = 'local', admin = 1; + "); + free_results($this->database); + } else { + $result->close(); + $this->database->query(" + UPDATE user SET auth_realm = 'local', active = 1 WHERE uid = 'keys-sync'; + "); + } +} + + +$this->database->multi_query(" +CREATE TABLE `entity_event_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `entity_id` int(10) unsigned NOT NULL, + `actor_id` int(10) unsigned, + `date` datetime NOT NULL, + `details` mediumtext NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_entity_event_entity_id` (`entity_id`), + KEY `FK_entity_event_actor_id` (`actor_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT entity_event_2 SELECT * FROM entity_event; + +DROP TABLE entity_event; +RENAME TABLE entity_event_2 TO entity_event; + +ALTER TABLE `entity_event` + ADD CONSTRAINT `FK_entity_event_actor_id` FOREIGN KEY (`actor_id`) REFERENCES `entity` (`id`) ON DELETE SET NULL, + ADD CONSTRAINT `FK_entity_event_entity_id` FOREIGN KEY (`entity_id`) REFERENCES `entity` (`id`) ON DELETE CASCADE; +"); +free_results($this->database); + + +$this->database->multi_query(" +CREATE TABLE `group_event_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `group` int(10) unsigned NOT NULL, + `entity_id` int(10) unsigned, + `date` datetime NOT NULL, + `details` mediumtext NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_group_event_group` (`group`), + KEY `FK_group_event_entity` (`entity_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT; + +INSERT group_event_2 SELECT * FROM group_event; + +DROP TABLE group_event; +RENAME TABLE group_event_2 TO group_event; + +ALTER TABLE `group_event` + ADD CONSTRAINT `FK_group_event_entity` FOREIGN KEY (`entity_id`) REFERENCES `entity` (`id`) ON DELETE SET NULL, + ADD CONSTRAINT `FK_group_event_group` FOREIGN KEY (`group`) REFERENCES `group` (`entity_id`) ON DELETE CASCADE; +"); +free_results($this->database); + + +$this->database->multi_query(" +CREATE TABLE `group_member_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `group` int(10) unsigned NOT NULL, + `entity_id` int(10) unsigned NOT NULL, + `add_date` datetime NOT NULL, + `added_by` int(10) unsigned, + PRIMARY KEY (`id`), + UNIQUE KEY `group_entity_id` (`group`, `entity_id`), + KEY `FK_group_member_entity` (`entity_id`), + KEY `FK_group_member_entity_2` (`added_by`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=COMPACT; + +INSERT group_member_2 SELECT * FROM group_member; + +DROP TABLE group_member; +RENAME TABLE group_member_2 TO group_member; + +ALTER TABLE `group_member` + ADD CONSTRAINT `FK_group_member_entity` FOREIGN KEY (`entity_id`) REFERENCES `entity` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `FK_group_member_entity_2` FOREIGN KEY (`added_by`) REFERENCES `entity` (`id`) ON DELETE SET NULL, + ADD CONSTRAINT `FK_group_member_group` FOREIGN KEY (`group`) REFERENCES `group` (`entity_id`) ON DELETE CASCADE +"); +free_results($this->database); + + +$this->database->multi_query(" +CREATE TABLE `server_event_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `server_id` int(10) unsigned NOT NULL, + `actor_id` int(10) unsigned, + `date` datetime NOT NULL, + `details` mediumtext NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_server_log_server` (`server_id`), + KEY `FK_server_event_actor_id` (`actor_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT server_event_2 SELECT * FROM server_event; + +DROP TABLE server_event; +RENAME TABLE server_event_2 TO server_event; + +ALTER TABLE `server_event` + ADD CONSTRAINT `FK_server_event_actor_id` FOREIGN KEY (`actor_id`) REFERENCES `entity` (`id`) ON DELETE SET NULL, + ADD CONSTRAINT `FK_server_log_server` FOREIGN KEY (`server_id`) REFERENCES `server` (`id`) ON DELETE CASCADE; +"); +free_results($this->database); + + +$this->database->multi_query(" +CREATE TABLE `server_note_2` ( + `id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `server_id` int(10) unsigned NOT NULL, + `entity_id` int(10) unsigned, + `date` datetime NOT NULL, + `note` mediumtext NOT NULL, + PRIMARY KEY (`id`), + KEY `FK_server_note_server` (`server_id`), + KEY `FK_server_note_user` (`entity_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +INSERT server_note_2 SELECT * FROM server_note; + +DROP TABLE server_note; +RENAME TABLE server_note_2 TO server_note; + +ALTER TABLE `server_note` + ADD CONSTRAINT `FK_server_note_entity` FOREIGN KEY (`entity_id`) REFERENCES `entity` (`id`) ON DELETE SET NULL, + ADD CONSTRAINT `FK_server_note_server` FOREIGN KEY (`server_id`) REFERENCES `server` (`id`) ON DELETE CASCADE +"); +free_results($this->database); + +$this->database->commit(); + +$this->database->autocommit(TRUE); diff --git a/model/migrationdirectory.php b/model/migrationdirectory.php index fabbc84..dd6a60c 100644 --- a/model/migrationdirectory.php +++ b/model/migrationdirectory.php @@ -22,7 +22,7 @@ class MigrationDirectory extends DBDirectory { /** * Increment this constant to activate a new migration from the migrations directory */ - const LAST_MIGRATION = 3; + const LAST_MIGRATION = 4; public function __construct() { parent::__construct(); diff --git a/model/user.php b/model/user.php index 6b7774e..deba94c 100644 --- a/model/user.php +++ b/model/user.php @@ -66,6 +66,23 @@ public function update() { } } + /** + * Delete the given user. + */ + public function delete() { + if(is_null($this->entity_id)) throw new BadMethodCallException('User must be in directory before it can be removed'); + $stmt = $this->database->prepare("DELETE FROM entity WHERE id = ?"); + $stmt->bind_param('d', $this->entity_id); + $stmt->execute(); + $stmt->close(); + $stmt = $this->database->prepare("DELETE FROM user WHERE entity_id = ?"); + $stmt->bind_param('d', $this->entity_id); + $stmt->execute(); + $stmt->close(); + + $this->sync_remote_access(); + } + /** * Magic getter method - if superior field requested, return User object of user's superior * @param string $field to retrieve diff --git a/model/userdirectory.php b/model/userdirectory.php index 8227b6b..5ff6b82 100644 --- a/model/userdirectory.php +++ b/model/userdirectory.php @@ -45,13 +45,22 @@ public function add_user(User $user) { $user_active = $user->active; $user_admin = $user->admin; $user_email = $user->email; - $stmt = $this->database->prepare("INSERT INTO entity SET type = 'user'"); - $stmt->execute(); - $user->entity_id = $stmt->insert_id; - $stmt = $this->database->prepare("INSERT INTO user SET entity_id = ?, uid = ?, name = ?, email = ?, active = ?, admin = ?"); - $stmt->bind_param('dsssdd', $user->entity_id, $user_id, $user_name, $user_email, $user_active, $user_admin); - $stmt->execute(); - $stmt->close(); + try { + $stmt = $this->database->prepare("INSERT INTO entity SET type = 'user'"); + $stmt->execute(); + $user->entity_id = $stmt->insert_id; + $stmt = $this->database->prepare("INSERT INTO user SET entity_id = ?, uid = ?, name = ?, email = ?, active = ?, admin = ?, auth_realm = ?"); + $stmt->bind_param('dsssdds', $user->entity_id, $user_id, $user_name, $user_email, $user_active, $user_admin, $user->auth_realm); + $stmt->execute(); + $stmt->close(); + } catch(mysqli_sql_exception $e) { + if($e->getCode() == 1062) { + // Duplicate entry + throw new UserAlreadyExistsException("User {$user->uid} already exists"); + } else { + throw $e; + } + } } /** @@ -82,6 +91,8 @@ public function get_user_by_id($id) { * @throws UserNotFoundException if no user with that uid exists */ public function get_user_by_uid($uid) { + global $config; + $ldap_enabled = $config['ldap']['enabled']; if(isset($this->cache_uid[$uid])) { return $this->cache_uid[$uid]; } @@ -93,11 +104,16 @@ public function get_user_by_uid($uid) { $user = new User($row['entity_id'], $row); $this->cache_uid[$uid] = $user; } else { - $user = new User; - $user->uid = $uid; - $this->cache_uid[$uid] = $user; - $user->get_details_from_ldap(); - $this->add_user($user); + if ($ldap_enabled == 1) { + $user = new User; + $user->uid = $uid; + $this->cache_uid[$uid] = $user; + $user->auth_realm = 'LDAP'; + $user->get_details_from_ldap(); + $this->add_user($user); + } else { + throw new UserNotFoundException('User does not exist.'); + } } $stmt->close(); return $user; @@ -148,3 +164,4 @@ public function list_users($include = array(), $filter = array()) { } class UserNotFoundException extends Exception {} +class UserAlreadyExistsException extends Exception {} diff --git a/requesthandler.php b/requesthandler.php index 5b921bf..cc60cd0 100644 --- a/requesthandler.php +++ b/requesthandler.php @@ -20,12 +20,6 @@ ob_start(); set_exception_handler('exception_handler'); -if(isset($_SERVER['PHP_AUTH_USER'])) { - $active_user = $user_dir->get_user_by_uid($_SERVER['PHP_AUTH_USER']); -} else { - throw new Exception("Not logged in."); -} - // Work out where we are on the server $base_path = dirname(__FILE__); $base_url = dirname($_SERVER['SCRIPT_NAME']); @@ -33,6 +27,17 @@ $relative_request_url = preg_replace('/^'.preg_quote($base_url, '/').'/', '/', $request_url); $absolute_request_url = 'http'.(isset($_SERVER['HTTPS']) ? 's' : '').'://'.$_SERVER['HTTP_HOST'].$request_url; +if(isset($_SERVER['PHP_AUTH_USER'])) { + try { + $active_user = $user_dir->get_user_by_uid($_SERVER['PHP_AUTH_USER']); + } catch(UserNotFoundException $ex) { + require('views/error403.php'); + die; + } +} else { + throw new Exception("Not logged in."); +} + if(empty($config['web']['enabled'])) { require('views/error503.php'); die; @@ -40,6 +45,7 @@ if(!$active_user->active) { require('views/error403.php'); + die; } if(!empty($_POST)) { @@ -62,10 +68,11 @@ if(isset($router->view)) { $view = path_join($base_path, 'views', $router->view.'.php'); if(file_exists($view)) { - if($active_user->auth_realm == 'LDAP' || $router->public) { + if($active_user->auth_realm == 'LDAP' || $active_user->auth_realm == 'local' || $router->public) { require($view); } else { require('views/error403.php'); + die; } } else { throw new Exception("View file $view missing."); diff --git a/scripts/ldap_update.php b/scripts/ldap_update.php index 6f8668f..076fca6 100755 --- a/scripts/ldap_update.php +++ b/scripts/ldap_update.php @@ -29,22 +29,28 @@ $active_user->uid = 'keys-sync'; $active_user->name = 'Synchronization script'; $active_user->email = ''; + $active_user->auth_realm = 'local'; $active_user->active = 1; $active_user->admin = 1; $active_user->developer = 0; $user_dir->add_user($active_user); } -try { - $sysgrp = $group_dir->get_group_by_name($config['ldap']['admin_group_cn']); -} catch(GroupNotFoundException $e) { - $sysgrp = new Group; - $sysgrp->name = $config['ldap']['admin_group_cn']; - $sysgrp->system = 1; - $group_dir->add_group($sysgrp); +$ldap_enabled = $config['ldap']['enabled']; + +if($ldap_enabled == 1) { + try { + $sysgrp = $group_dir->get_group_by_name($config['ldap']['admin_group_cn']); + } catch(GroupNotFoundException $e) { + $sysgrp = new Group; + $sysgrp->name = $config['ldap']['admin_group_cn']; + $sysgrp->system = 1; + $group_dir->add_group($sysgrp); + } } + foreach($users as $user) { - if($user->auth_realm == 'LDAP') { + if($user->auth_realm == 'LDAP' && $ldap_enabled == 1) { $active = $user->active; try { $user->get_details_from_ldap(); diff --git a/scripts/sync.php b/scripts/sync.php index 208818b..1390e32 100755 --- a/scripts/sync.php +++ b/scripts/sync.php @@ -69,6 +69,7 @@ $active_user->uid = 'keys-sync'; $active_user->name = 'Synchronization script'; $active_user->email = ''; + $active_user->auth_realm = 'local'; $active_user->active = 1; $active_user->admin = 1; $active_user->developer = 0; diff --git a/templates/functions.php b/templates/functions.php index aaf4184..758d395 100644 --- a/templates/functions.php +++ b/templates/functions.php @@ -85,7 +85,11 @@ function show_event($event) { group->name) ?> + actor->uid)) { ?> + removed + actor->uid) ?> + date) ?> diff --git a/templates/group.php b/templates/group.php index cd0a6d9..569c116 100644 --- a/templates/group.php +++ b/templates/group.php @@ -82,7 +82,7 @@ break; } ?> - Added on add_date) ?> by added_by->uid) ?> + Added on add_date) ?> by added_by->uid)) { ?>removedadded_by->uid) ?> get('group')->system) { ?> diff --git a/templates/server.php b/templates/server.php index 2c493d7..7e11bb9 100644 --- a/templates/server.php +++ b/templates/server.php @@ -451,7 +451,7 @@
get('output_formatter')->comment_format($note->note), ESC_NONE)?>
diff --git a/templates/user.php b/templates/user.php index a4f8d7e..c7f18c7 100644 --- a/templates/user.php +++ b/templates/user.php @@ -217,6 +217,17 @@ + get('user')->auth_realm == 'local' && $this->get('admin')) { ?> +

User managment

+
+ get('active_user')->get_csrf_field(), ESC_NONE) ?> +
+
+ +
+
+
+ get('user')->auth_realm == 'LDAP') { ?>
diff --git a/templates/users.php b/templates/users.php index 072303a..1d13ad0 100644 --- a/templates/users.php +++ b/templates/users.php @@ -16,21 +16,59 @@ ## ?>

Users

- - - - - - - - - - get('users') as $user) { ?> - active) out(' class="text-muted"', ESC_NONE) ?>> - - - - - - -
UsernameFull namePublic keys
uid)?>name)?>list_public_keys())))?>
+get('admin')) { ?> + + + + + +
+
+

User list

+

get('users')); out(number_format($total).' user'.($total == 1 ? '' : 's').' found')?>

+ + + + + + + + + + get('users') as $user) { ?> + active) out(' class="text-muted"', ESC_NONE) ?>> + + + + + + +
UsernameFull namePublic keys
uid)?>name)?>list_public_keys())))?>
+
+ + get('admin')) { ?> +
+

Add user

+
+ get('active_user')->get_csrf_field(), ESC_NONE) ?> +
+ + +
+
+ + +
+
+ + +
+ Administrator

+ +
+
+ +
diff --git a/views/user.php b/views/user.php index f871308..1f59f50 100644 --- a/views/user.php +++ b/views/user.php @@ -48,9 +48,16 @@ } } elseif(isset($_POST['edit_user']) && $active_user->admin) { $user->force_disable = $_POST['force_disable']; - $user->get_details_from_ldap(); + if($active_user->auth_realm == 'LDAP' ) { + $user->get_details_from_ldap(); + } $user->update(); redirect('#settings'); +} elseif(isset($_POST['delete_user']) && $active_user->admin) { + if($user->auth_realm == 'local' && $user->uid != 'keys-sync' ) { + $user->delete(); + } + redirect('/users'); } else { $content = new PageSection('user'); $content->set('user', $user); diff --git a/views/users.php b/views/users.php index 72ddf8b..c129b79 100644 --- a/views/users.php +++ b/views/users.php @@ -15,10 +15,44 @@ ## limitations under the License. ## -$content = new PageSection('users'); -$content->set('users', $user_dir->list_users()); -$content->set('admin', $active_user->admin); +if(isset($_POST['add_user']) && $active_user->admin) { + $uid = trim($_POST['uid']); + $name = trim($_POST['name']); + $email = trim($_POST['email']); + + $user = new User; + $user->uid = $uid; + $user->name = $name; + $user->email = $email; + + $user->active = 1; + if (isset($_POST['admin']) && $_POST['admin'] === 'admin') { + $user->admin = 1; + } else { + $user->admin = 0; + } + $user->auth_realm = 'local'; + try { + $user_dir->add_user($user); + $alert = new UserAlert; + $alert->content = 'User \''.hesc($user->uid).'\' successfully created.'; + $alert->escaping = ESC_NONE; + $active_user->add_alert($alert); + } catch(UserAlreadyExistsException $e) { + $alert = new UserAlert; + $alert->content = 'User \''.hesc($user->uid).'\' is already known by SSH Key Authority.'; + $alert->escaping = ESC_NONE; + $alert->class = 'danger'; + $active_user->add_alert($alert); + } + redirect('#add'); +} else { + $content = new PageSection('users'); + $content->set('users', $user_dir->list_users()); + $content->set('admin', $active_user->admin); +} + $page = new PageSection('base'); $page->set('title', 'Users'); $page->set('content', $content);