[ROSADMIN] Add Mattermost plugin for admin panel

This commit is contained in:
Stas'M 2020-12-11 15:16:46 +03:00 committed by Colin Finck
parent e6a9b171cd
commit 121dd77b1b
8 changed files with 491 additions and 0 deletions

View File

@ -0,0 +1,79 @@
<?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: Curl Helper Class for RosAdmin
* COPYRIGHT: Copyright 2020 Stanislav Motylkov (x86corez@gmail.com)
*/
class CurlHelper
{
//
// MEMBER VARIABLES
//
private $_last_error;
private $_custom_headers;
//
// PRIVATE FUNCTIONS
//
private function perform($url, $content_type, $content_data)
{
$headers = $this->_custom_headers;
if (!empty($content_type))
$headers[] = "Content-Type: {$content_type}";
$ver = phpversion();
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_ENCODING, 'utf-8');
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
curl_setopt($ch, CURLOPT_FAILONERROR, 0);
curl_setopt($ch, CURLOPT_USERAGENT, "PHP/{$ver} RosAdmin Curl");
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if (!empty($content_type))
curl_setopt($ch, CURLOPT_POSTFIELDS, $content_data);
$data = curl_exec($ch);
if (!$data)
$this->_last_error = curl_error($ch);
curl_close($ch);
return $data;
}
//
// PUBLIC FUNCTIONS
//
public function __construct()
{
$this->_last_error = null;
$this->_custom_headers = array();
}
public function get($url)
{
return $this->perform($url, '', '');
}
public function post($url, $content_type, $content_data)
{
return $this->perform($url, $content_type, $content_data);
}
public function setHeaders($headers)
{
$this->_custom_headers = $headers;
}
public function getLastError()
{
$error = $this->_last_error;
$this->_last_error = null;
return $error;
}
}

View File

@ -0,0 +1,228 @@
<?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: Mattermost communication with RosAdmin
* COPYRIGHT: Copyright 2020 Stanislav Motylkov (x86corez@gmail.com)
*/
class Mattermost
{
//
// MEMBER VARIABLES
//
private $_ch;
private $_url;
private $_last_error;
//
// PRIVATE FUNCTIONS
//
/**
* Is this error returned by Mattermost server?
*/
private function isError($json)
{
return (isset($json['message']) && isset($json['status_code']));
}
/**
* Generates unique to the client temporary filename
*/
private function getTempFilename()
{
$uniq = md5($_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']);
return sys_get_temp_dir() . '/rosadmin_mattermost_' . substr($uniq, 0, 8);
}
/**
* Reads error message that was saved earlier
* and removes it from disk
*/
private function restoreError()
{
$filename = Mattermost::getTempFilename();
if (!file_exists($filename))
return null;
$error = file_get_contents($filename);
unlink($filename);
return $error;
}
//
// PUBLIC FUNCTIONS
//
public function __construct($curl_helper, $url, $token)
{
$this->_ch = $curl_helper;
$this->_url = $url;
$this->_ch->setHeaders(array("Authorization: Bearer {$token}"));
$this->_last_error = null;
}
/**
* Returns user ID
*/
public function getUserByName($name)
{
/* Mattermost usernames are lower-case */
$name = urlencode(strtolower($name));
$json = $this->_ch->get("{$this->_url}/api/v4/users/username/{$name}");
if (!$json)
{
$this->_last_error = $this->_ch->getLastError();
return null;
}
$json = json_decode($json, true);
if (Mattermost::isError($json))
{
$this->_last_error = $json['message'];
if ($json['status_code'] == 404)
$this->_last_error = 'User is not registered in Mattermost';
return null;
}
else if (!isset($json['id']) && empty($this->_last_error))
{
/* Request was successful, but response is not recognized */
$this->_last_error = 'Cannot connect to Mattermost';
return null;
}
return $json['id'];
}
/**
* Returns user sessions
*/
public function getSessionsById($user_id)
{
$user_id = urlencode($user_id);
$json = $this->_ch->get("{$this->_url}/api/v4/users/{$user_id}/sessions");
if (!$json)
{
$this->_last_error = $this->_ch->getLastError();
return null;
}
$json = json_decode($json, true);
if (Mattermost::isError($json))
{
$this->_last_error = $json['message'];
return null;
}
return $json;
}
/**
* Returns user audits
*/
public function getAuditsById($user_id)
{
$user_id = urlencode($user_id);
$json = $this->_ch->get("{$this->_url}/api/v4/users/{$user_id}/audits");
if (!$json)
{
$this->_last_error = $this->_ch->getLastError();
return null;
}
$json = json_decode($json, true);
if (Mattermost::isError($json))
{
$this->_last_error = $json['message'];
return null;
}
return $json;
}
/**
* Revokes user session
*/
public function revokeSession($user_id, $session_id)
{
$user_id = urlencode($user_id);
$json = $this->_ch->post(
"{$this->_url}/api/v4/users/{$user_id}/sessions/revoke",
"application/json",
json_encode(array('session_id' => $session_id)));
if (!$json)
{
$this->_last_error = $this->_ch->getLastError();
return null;
}
$json = json_decode($json, true);
if (Mattermost::isError($json))
{
$this->_last_error = $json['message'];
return null;
}
return $json;
}
/**
* Revokes all user sessions
*/
public function revokeAllSessions($user_id)
{
$user_id = urlencode($user_id);
$json = $this->_ch->post(
"{$this->_url}/api/v4/users/{$user_id}/sessions/revoke/all",
"application/json",
"{}");
if (!$json)
{
$this->_last_error = $this->_ch->getLastError();
return null;
}
$json = json_decode($json, true);
if (Mattermost::isError($json))
{
$this->_last_error = $json['message'];
return null;
}
return $json;
}
/**
* Returns all IP addresses associated with a session
* using audit information
*/
public function getSessionAddresses($audits, $session_id)
{
$ips = array();
for ($i = 0; $i < count($audits); $i++)
{
$e = $audits[$i];
if ($e['session_id'] === $session_id)
{
$ip = $e['ip_address'];
if (!in_array($ip, $ips, true))
$ips[] = $ip;
}
}
return $ips;
}
/**
* Writes error message to disk to display it later
*/
public function rememberError()
{
$error = $this->getLastError();
$filename = Mattermost::getTempFilename();
file_put_contents($filename, $error);
return $error;
}
/**
* Gets textual description of last encountered error
*/
public function getLastError()
{
$error = $this->_last_error;
$this->_last_error = null;
if (is_null($error))
$error = Mattermost::restoreError();
return $error;
}
}

View File

@ -16,6 +16,8 @@
private $_is_mod;
private $_is_admin;
public $mm;
//
// PRIVATE FUNCTIONS
//
@ -43,6 +45,8 @@
// Let's hope this never happens
if ($this->_userinfo['banned'])
redirect_to('/roslogin/?p=login');
$this->mm = new Mattermost(new CurlHelper(), ROSLOGIN_MATTERMOST_URL, ROSLOGIN_MATTERMOST_TOKEN);
}

View File

@ -0,0 +1,36 @@
<?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: Revoking a user session
* COPYRIGHT: Copyright 2020 Stanislav Motylkov (x86corez@gmail.com)
*/
class RevokeAction
{
public function perform($ra)
{
if (!array_key_exists('username', $_POST)
|| !array_key_exists('user_id', $_POST)
|| !array_key_exists('session_id', $_POST))
{
throw new RuntimeException("Necessary information not specified");
}
$username = $_POST['username'];
$user_id = $_POST['user_id'];
$session_id = $_POST['session_id'];
$data = [
'username' => $username,
];
$result = $ra->mm->revokeSession($user_id, $session_id);
if (!$result || $result['status'] != "OK")
{
$ra->mm->rememberError();
redirect_to("?p=user&revoke_problem=1&" . http_build_query($data));
}
else
redirect_to("?p=user&revoke_ok=1&" . http_build_query($data));
}
}

View File

@ -0,0 +1,34 @@
<?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: Revoking all user sessions
* COPYRIGHT: Copyright 2020 Stanislav Motylkov (x86corez@gmail.com)
*/
class RevokeAllAction
{
public function perform($ra)
{
if (!array_key_exists('username', $_POST)
|| !array_key_exists('user_id', $_POST))
{
throw new RuntimeException("Necessary information not specified");
}
$username = $_POST['username'];
$user_id = $_POST['user_id'];
$data = [
'username' => $username,
];
$result = $ra->mm->revokeAllSessions($user_id);
if (!$result || $result['status'] != "OK")
{
$ra->mm->rememberError();
redirect_to("?p=user&revoke_all_problem=1&" . http_build_query($data));
}
else
redirect_to("?p=user&revoke_all_ok=1&" . http_build_query($data));
}
}

View File

@ -12,6 +12,8 @@
define("ROOT_PATH", "../../");
require_once("../RosLogin.php");
require_once("RosAdmin.php");
require_once("CurlHelper.php");
require_once("Mattermost.php");
require_once(ROOT_PATH . "rosweb/rosweb.php");
$rw = new RosWeb();
@ -65,6 +67,14 @@
$action = new BanAction();
break;
case "revoke":
$action = new RevokeAction();
break;
case "revoke_all":
$action = new RevokeAllAction();
break;
default:
throw new RuntimeException("Invalid action");
}

View File

@ -71,6 +71,39 @@
$unbanned = array_key_exists("unbanned", $_GET);
$was_not_banned = array_key_exists("was_not_banned", $_GET);
$revoke_ok = array_key_exists("revoke_ok", $_GET);
$revoke_all_ok = array_key_exists("revoke_all_ok", $_GET);
$revoke_problem = array_key_exists("revoke_problem", $_GET);
$revoke_all_problem = array_key_exists("revoke_all_problem", $_GET);
if ($revoke_problem || $revoke_all_problem)
$revoke_error = $this->_ra->mm->getLastError();
$chat_sessions = null;
$chat_audits = null;
$chat_user_id = $this->_ra->mm->getUserByName($_GET["username"]);
if (!is_null($chat_user_id))
{
$chat_sessions = $this->_ra->mm->getSessionsById($chat_user_id);
$chat_audits = $this->_ra->mm->getAuditsById($chat_user_id);
}
$chat_message = "";
if (is_null($chat_user_id) || is_null($chat_sessions) || is_null($chat_audits))
{
$chat_message = $this->_ra->mm->getLastError();
if ($chat_message != "User is not registered in Mattermost")
{
if (is_null($chat_user_id))
$chat_message = "Failed to get user ID: {$chat_message}";
else if (is_null($chat_sessions))
$chat_message = "Failed to get user sessions: {$chat_message}";
else if (is_null($chat_audits))
$chat_message = "Failed to get user audits: {$chat_message}";
}
}
else if (count($chat_sessions) == 0)
{
$chat_message = "User has no active Mattermost sessions";
}
$user_message = "";
if ($banned || $already_banned || $unbanned || $was_not_banned)
{
@ -83,6 +116,17 @@
else if ($was_not_banned)
$user_message = "User was not banned";
}
else if ($revoke_ok || $revoke_all_ok || $revoke_problem || $revoke_all_problem)
{
if ($revoke_ok)
$user_message = "User session has been revoked";
else if ($revoke_all_ok)
$user_message = "All user sessions has been revoked";
else if ($revoke_problem)
$user_message = "Failed to revoke user session: {$revoke_error}";
else if ($revoke_all_problem)
$user_message = "Failed to revoke all user sessions: {$revoke_error}";
}
if (!empty($user_message))
{
@ -140,6 +184,60 @@
</form>
</tr>
</table>
<p class="lead center col-md-9">Mattermost Sessions</p>
<?php
if (!empty($chat_message))
{
?>
<div class="col-md-10 col-md-offset-1"><?php echo $chat_message; ?></div>
<?php
}
else
{
?>
<table class="col-md-offset-1 table table-striped table-bordered">
<tr>
<th>
<form class="form-horizontal" method="post">
<input type="hidden" name="username" value="<?php echo $username; ?>">
<input type="hidden" name="user_id" value="<?php echo $chat_user_id; ?>">
<button type="submit" name="a" value="revoke_all" class="btn btn-danger">Revoke All</button>
</form>
</th>
<th>Token</th>
<th>IP Addresses</th>
<th>User Agent</th>
</tr>
<?php
for ($i = 0; $i < count($chat_sessions); $i++)
{
$e = $chat_sessions[$i];
$ua = $e['props']['os'];
if (empty($ua))
$ua = $e['props']['platform'];
if (!empty($ua))
$ua .= ' - ';
$ua .= $e['props']['browser'];
?>
<tr>
<td>
<form class="form-horizontal" method="post">
<input type="hidden" name="username" value="<?php echo $username; ?>">
<input type="hidden" name="user_id" value="<?php echo $chat_user_id; ?>">
<input type="hidden" name="session_id" value="<?php echo $e['id']; ?>">
<button type="submit" name="a" value="revoke" class="btn btn-warning">Revoke</button>
</form>
</td>
<td><? echo $e['token']; ?></td>
<td><? echo implode('<br>', $this->_ra->mm->getSessionAddresses($chat_audits, $e['id'])); ?></td>
<td><? echo htmlspecialchars($ua); ?></td>
</tr>
<?php
}
?>
</table>
<?php
}
}
}

View File

@ -32,3 +32,5 @@
// Admin panel settings
define("ROSLOGIN_ADMIN_GROUP", "LDAP Administrators");
define("ROSLOGIN_MODERATOR_GROUP", "Moderators");
define("ROSLOGIN_MATTERMOST_URL", "https://chat.reactos.org");
define("ROSLOGIN_MATTERMOST_TOKEN", "YOUR_MATTERMOST_TOKEN_HERE");