[ROSADMIN] Add basic admin panel

Security review / fixes by colin

Co-authored-by: Colin Finck <colin@reactos.org>
This commit is contained in:
Mark Jansen 2020-10-03 14:15:22 +02:00 committed by Colin Finck
parent 79104a1171
commit d5373bc57a
6 changed files with 613 additions and 0 deletions

View File

@ -0,0 +1,199 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: Internal implementation of admin features
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
class RosAdmin extends RosLogin
{
//
// MEMBER VARIABLES
//
private $_username;
private $_userinfo;
private $_is_mod;
private $_is_admin;
//
// PRIVATE FUNCTIONS
//
//
// PUBLIC FUNCTIONS
//
public function __construct()
{
$this->_username = $this->isLoggedIn();
// Forward to the Login page if the user is not logged in.
if (!$this->_username)
redirect_to('/roslogin/?p=login&redirect=/roslogin/admin');
$this->_userinfo = $this->getUserWithGroupInformation($this->_username);
$this->_is_mod = RosAdmin::checkMod($this->_userinfo);
$this->_is_admin = RosAdmin::checkAdmin($this->_userinfo);
// Admins are also moderators
if (!$this->_is_mod)
redirect_to('/roslogin/?p=login');
// Let's hope this never happens
if ($this->_userinfo['banned'])
redirect_to('/roslogin/?p=login');
}
public function banUser($username, $userinfo)
{
if (!$this->canBan($userinfo))
throw new RuntimeException('Cannot ban this user');
// Connect to LDAP with the service account
$this->_connectToLDAP();
$dn = $this->_getUserNameDN($username);
$info = [
'userPassword' => base64_encode(random_bytes(40)),
'mail' => $userinfo['email'] . '.disabled',
];
if (!ldap_mod_replace($this->_ds, $dn, $info))
throw new RuntimeException('Could not modify the password / email in the LDAP directory');
// Delete the session from the database.
$this->_connectToDB();
$stmt = $this->_dbh->prepare('DELETE FROM sessions WHERE username = :username');
$stmt->bindParam(':username', $username);
$stmt->execute();
}
public function unbanUser($username, $userinfo)
{
if (!$this->canBan($userinfo))
throw new RuntimeException('Cannot ban this user');
// Connect to LDAP with the service account
$this->_connectToLDAP();
// Restore the original email
$dn = $this->_getUserNameDN($username);
$info = [
'mail' => $userinfo['email'], // The $userinfo has a stripped email address!
];
if (!ldap_mod_replace($this->_ds, $dn, $info))
throw new RuntimeException('Could not modify the email in the LDAP directory');
// Send a reset password email
// The text is enlgish, since we do not know the banned user's locale
$mailtemplate = file_get_contents('../lang/en_resetpassword_mail.txt');
$this->resetPasswordRequest($username, 'Reset Account Password', $mailtemplate);
}
/**
* Can the currently logged in user ban the specified $userinfo?
*/
public function canBan($userinfo)
{
if (RosAdmin::checkAdmin($userinfo))
{
// Admins cannot be banned
return false;
}
else if (RosAdmin::checkMod($userinfo))
{
// Only admins can ban moderators
return $this->isAdmin();
}
return true;
}
/**
* Finds all banned users
* These are indicated with a .disabled suffix on the email
*/
public function getBannedUsers()
{
// Connect to LDAP using the service account.
$this->_connectToLDAP();
$filter = '(mail=*.disabled)';
$sr = ldap_search($this->_ds, ROSLOGIN_LDAP_BASE_DN, $filter);
$info = ldap_get_entries($this->_ds, $sr);
unset($info['count']);
return $info;
}
public function getUserWithGroupInformation($username)
{
$info = parent::getUserInformation($username, ['memberOf']);
// Flatten the ldap-returned data
$groups = $info['memberof'];
$memberof = array();
for ($i = 0; $i < $groups['count']; $i++)
{
// Grab the value of the first entry (cn)
// cn=Moderators,ou=Groups,o=ReactOS Website
$memberof[] = ldap_explode_dn($groups[$i], 1)[0];
}
$info['memberof'] = $memberof;
// clean up the email address, and add 'banned' info
$disabled_str = '.disabled';
$disabled_len = strlen($disabled_str);
if (strlen($info['email']) >= $disabled_len && substr_compare($info['email'], $disabled_str, -$disabled_len) === 0)
{
$info['email'] = substr($info['email'], 0, -$disabled_len);
$info['banned'] = true;
}
else
{
$info['banned'] = false;
}
return $info;
}
public function isMod()
{
if ($this->_is_mod)
return true;
// Admins are also moderators
return $this->isAdmin();
}
public function isAdmin()
{
return $this->_is_admin;
}
public static function checkMod($userinfo)
{
if (in_array(ROSLOGIN_MODERATOR_GROUP, $userinfo['memberof']))
{
return true;
}
// Admins are also moderators
return RosAdmin::checkAdmin($userinfo);
}
public static function checkAdmin($userinfo)
{
return in_array(ROSLOGIN_ADMIN_GROUP, $userinfo['memberof']);
}
public function userTitle()
{
if ($this->isAdmin())
return 'Administrator';
if ($this->isMod())
return 'Moderator';
return 'User';
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: Banning a user
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
class BanAction
{
public function perform($ra)
{
if (!array_key_exists('username', $_POST))
{
throw new RuntimeException("Necessary information not specified");
}
$username = $_POST['username'];
$data = [
'username' => $username,
];
try
{
$userinfo = $ra->getUserWithGroupInformation($username);
if ($userinfo['banned'])
{
redirect_to("?p=user&already_banned=1&" . http_build_query($data));
}
$ra->banUser($username, $userinfo);
redirect_to("?p=user&banned=1&" . http_build_query($data));
}
catch (InvalidUserNameException $e)
{
// Unknown user, back to home!
redirect_to("?p=home&invalid_username=1&" . http_build_query($data));
}
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: Unbanning a user
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
class UnbanAction
{
public function perform($ra)
{
if (!array_key_exists('username', $_POST))
{
throw new RuntimeException("Necessary information not specified");
}
$username = $_POST['username'];
$data = [
'username' => $username
];
try
{
$userinfo = $ra->getUserWithGroupInformation($username);
if (!$userinfo['banned'])
{
redirect_to("?p=user&was_not_banned=1&" . http_build_query($data));
}
$ra->unbanUser($username, $userinfo);
redirect_to("?p=user&unbanned=1&" . http_build_query($data));
}
catch (InvalidUserNameException $e)
{
// Unknown user, back to home!
redirect_to("?p=home&invalid_username=1&" . http_build_query($data));
}
}
}

View File

@ -0,0 +1,102 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: The admin panel for RosLogin
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
//
// INCLUDES
//
define("ROOT_PATH", "../../");
require_once("../RosLogin.php");
require_once("RosAdmin.php");
require_once(ROOT_PATH . "rosweb/rosweb.php");
$rw = new RosWeb();
//
// FUNCTIONS
//
function load_action_class($class)
{
require_once("actions/{$class}.php");
}
function load_page_class($class)
{
require_once("pages/{$class}.php");
}
function redirect_to($url)
{
header("Location: {$url}");
exit;
}
//
// ENTRY POINT
//
// Redirect all plain HTTP requests to secure HTTPS URLs.
if (!array_key_exists("HTTPS", $_SERVER) || $_SERVER["HTTPS"] != "on")
redirect_to("https://" . $_SERVER["SERVER_NAME"] . $_SERVER["REQUEST_URI"]);
// If the user does not have moderation rights, or is not logged in,
// this will block them from going any further!
$ra = new RosAdmin();
try
{
if (array_key_exists("a", $_REQUEST))
{
spl_autoload_register("load_action_class");
switch ($_REQUEST["a"])
{
case "unban":
$action = new UnbanAction();
break;
case "ban":
$action = new BanAction();
break;
default:
throw new RuntimeException("Invalid action");
}
$action->perform($ra);
}
else if (array_key_exists("p", $_REQUEST))
{
spl_autoload_register("load_page_class");
switch ($_REQUEST["p"])
{
case "home":
$page = new AdminPage($ra);
break;
case "user":
$page = new UserPage($ra);
break;
default:
throw new RuntimeException("Invalid page");
}
require_once("../page.php");
}
else
{
// Redirect to the Login page if this file was called without any arguments.
redirect_to("?p=home");
}
}
catch (Exception $e)
{
die($e->getFile() . ":" . $e->getLine() . " - " . $e->getMessage());
}

View File

@ -0,0 +1,89 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: The main admin page for RosLogin
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
class AdminPage
{
private $_ra;
private $_banned_users;
public function __construct($ra)
{
$this->_ra = $ra;
$this->_banned_users = $this->_ra->getBannedUsers();
}
public function printBreadcrumbs()
{
echo " / <a href=\"/roslogin/admin\">admin</a>";
}
public function printTitle()
{
echo $this->_ra->userTitle() . " - home";
}
public function printHead()
{
}
public function printContent()
{
$username = array_key_exists("username", $_GET) ? htmlspecialchars($_GET["username"]) : "";
$invalid_username = array_key_exists("invalid_username", $_GET);
?>
<p class="lead center col-md-10">Search user</p>
<form class="form-horizontal" method="get">
<input type="hidden" name="p" value="user">
<div class="col-md-offset-1 col-md-8">
<fieldset class="form-group">
<div class="form-group <?php if ($invalid_username) { echo "has-error"; } ?>">
<label for="username" class="col-md-4 control-label">Username</label>
<div class="col-md-8">
<input required class="form-control" type="text" name="username">
<?php
if ($invalid_username)
echo "<span class=\"help-block\">Invalid username ($username)</span>";
?>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-4 col-md-8">
<button type="submit" class="btn btn-primary">Search</button>
</div>
</div>
</fieldset>
</div>
</form>
<hr>
<p></p>
<p class="lead center col-md-10">Banned users</p>
<table class="col-md-offset-1 table table-striped table-bordered">
<tr><th class="col-md-3">Username</th><th class="col-md-3">Display Name</th><th></th></tr>
<?php
foreach ($this->_banned_users as $user)
{
$cn = htmlspecialchars($user['cn'][0]);
$dn = htmlspecialchars($user['displayname'][0]);
echo "<tr><td class=\"col-md-3\">$cn</td><td class=\"col-md-3\">$dn</td><td>";
?>
<form class="form-horizontal" method="get">
<input type="hidden" name="p" value="user">
<input type="hidden" name="username" value="<?php echo $cn; ?>">
<button type="submit" class="btn btn-info">View</button>
</form>
<?php
echo "</td></tr>";
}
?>
</table>
<?php
}
}

View File

@ -0,0 +1,141 @@
<?php
/*
* PROJECT: RosLogin - A simple Self-Service and Single-Sign-On around an LDAP user directory
* LICENSE: AGPL-3.0-or-later (https://spdx.org/licenses/AGPL-3.0-or-later)
* PURPOSE: The main admin page for RosLogin
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
*/
class UserPage
{
private $_ra;
private $_userinfo;
private function _checkMark($value, $color)
{
if ($value)
return "<i style=\"color: $color;\" class=\"far fa-check-circle\"></i>";
return "";
}
public function __construct($ra)
{
if (!array_key_exists("username", $_GET))
{
throw new RuntimeException("Necessary information not specified");
}
$this->_ra = $ra;
$data = [
"username" => $_GET["username"],
];
try
{
$this->_userinfo = $this->_ra->getUserWithGroupInformation($_GET["username"]);
}
catch (InvalidUserNameException $e)
{
// Search failed, so redirect back
redirect_to("?p=home&invalid_username=1&" . http_build_query($data));
}
}
public function printBreadcrumbs()
{
echo " / <a href=\"/roslogin/admin\">admin</a>";
}
public function printTitle()
{
echo $this->_ra->userTitle() . " - viewing user";
}
public function printHead()
{
}
public function printContent()
{
$info = $this->_userinfo;
$is_mod = RosAdmin::checkMod($info);
$is_admin = RosAdmin::checkAdmin($info);
$can_ban = $this->_ra->canBan($info);
$username = htmlspecialchars($_GET["username"]);
$displayname = htmlspecialchars($info["displayname"]);
$email = htmlspecialchars($info["email"]);
$banned = array_key_exists("banned", $_GET);
$already_banned = array_key_exists("already_banned", $_GET);
$unbanned = array_key_exists("unbanned", $_GET);
$was_not_banned = array_key_exists("was_not_banned", $_GET);
if ($banned || $already_banned || $unbanned || $was_not_banned)
{
?>
<p class="lead center col-md-9">Message</p>
<form>
<div class="form-group col-md-offset-1 col-md-8">
<label class="control-label">
<?php
if ($banned)
echo "User has been banned";
else if ($already_banned)
echo "User has already been banned";
else if ($unbanned)
echo "User has been unbanned and a Reset Password email sent";
else if ($was_not_banned)
echo "User was not banned";
?>
</label>
</div>
</form>
<?php
}
?>
<p class="lead center col-md-9">User Information</p>
<table class="col-md-offset-1 table table-striped table-bordered">
<tr>
<th class="col-md-3">Username</th><td><?php echo $username; ?></td>
</tr>
<tr>
<th class="col-md-3">Display Name</th><td><?php echo $displayname; ?></td>
</tr>
<tr>
<th class="col-md-3">E-Mail Address</th><td><?php echo $email; ?></td>
</tr>
<tr>
<th class="col-md-3">Banned</th><td><?php echo $this->_checkMark($info['banned'], 'red'); ?></td>
</tr>
<tr>
<th class="col-md-3">Moderator</th><td><?php echo $this->_checkMark($is_mod, 'green'); ?></td>
</tr>
<tr>
<th class="col-md-3">Administrator</th><td><?php echo $this->_checkMark($is_admin, 'green'); ?></td>
</tr>
<tr>
<form class="form-horizontal" method="post">
<input type="hidden" name="username" value="<?php echo $username; ?>">
<th class="col-md-3">Actions</th><td><?php
if ($info['banned'])
{
?>
<button type="submit" name="a" value="unban" class="btn btn-warning" <?php if (!$can_ban) { echo "disabled"; } ?>>Unban</button>
<?php
}
else
{
?>
<button type="submit" name="a" value="ban" class="btn btn-danger" <?php if (!$can_ban) { echo "disabled"; } ?>>Ban</button>
<?php
}
?></td>
</form>
</tr>
</table>
<?php
}
}