mirror of
https://github.com/reactos/web.git
synced 2024-11-26 21:20:28 +00:00
[ROSADMIN] Add basic admin panel
Security review / fixes by colin Co-authored-by: Colin Finck <colin@reactos.org>
This commit is contained in:
parent
79104a1171
commit
d5373bc57a
199
www/www.reactos.org/roslogin/admin/RosAdmin.php
Normal file
199
www/www.reactos.org/roslogin/admin/RosAdmin.php
Normal 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';
|
||||
}
|
||||
|
||||
}
|
41
www/www.reactos.org/roslogin/admin/actions/BanAction.php
Normal file
41
www/www.reactos.org/roslogin/admin/actions/BanAction.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
41
www/www.reactos.org/roslogin/admin/actions/UnbanAction.php
Normal file
41
www/www.reactos.org/roslogin/admin/actions/UnbanAction.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
102
www/www.reactos.org/roslogin/admin/index.php
Normal file
102
www/www.reactos.org/roslogin/admin/index.php
Normal 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());
|
||||
}
|
89
www/www.reactos.org/roslogin/admin/pages/AdminPage.php
Normal file
89
www/www.reactos.org/roslogin/admin/pages/AdminPage.php
Normal 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
|
||||
}
|
||||
}
|
141
www/www.reactos.org/roslogin/admin/pages/UserPage.php
Normal file
141
www/www.reactos.org/roslogin/admin/pages/UserPage.php
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user