Skip to content
Snippets Groups Projects
Verified Commit c5b5b41f authored by Dominik Frantisek Bucik's avatar Dominik Frantisek Bucik Committed by Dominik František Bučík
Browse files

fix: :bug: Fix possible exceptions and refactor code

Refactored code to include more logging. Also fixed some possible
exception places.
parent 7e001acf
No related branches found
No related tags found
1 merge request!82fix: 🐛 Fix possible exceptions
......@@ -5,11 +5,17 @@ declare(strict_types=1);
namespace SimpleSAML\Module\proxystatistics\Auth\Process;
use DateTime;
use Exception;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Logger;
use SimpleSAML\Module\proxystatistics\DatabaseCommand;
class Statistics extends ProcessingFilter
{
private const STAGE = 'proxystatistics:Statistics';
private const DEBUG_PREFIX = self::STAGE . ' - ';
public function __construct($config, $reserved)
{
parent::__construct($config, $reserved);
......@@ -19,6 +25,12 @@ class Statistics extends ProcessingFilter
{
$dateTime = new DateTime();
$dbCmd = new DatabaseCommand();
$dbCmd->insertLogin($request, $dateTime);
try {
$dbCmd->insertLogin($request, $dateTime);
} catch (Exception $ex) {
Logger::error(
self::DEBUG_PREFIX . 'Caught exception while inserting login into statistics: ' . $ex->getMessage()
);
}
}
}
......@@ -20,6 +20,8 @@ class Config
public const MODE_PROXY = 'PROXY';
private const KNOWN_MODES = ['PROXY', 'IDP', 'SP', 'MULTI_IDP'];
private const STORE = 'store';
private const MODE = 'mode';
......@@ -40,22 +42,33 @@ class Config
private $sourceIdpEntityIdAttribute;
private $tables;
private $keepPerUser;
private $requiredAuthSource;
private $idAttribute;
private static $instance;
private function __construct()
{
$this->config = Configuration::getConfig(self::CONFIG_FILE_NAME);
$this->store = $this->config->getConfigItem(self::STORE, null);
$this->tables = $this->config->getArray('tables', []);
$this->mode = $this->config->getValueValidate(self::MODE, ['PROXY', 'IDP', 'SP', 'MULTI_IDP'], 'PROXY');
$this->tables = $this->config->getArray('tables', []);;
$this->sourceIdpEntityIdAttribute = $this->config->getString(self::SOURCE_IDP_ENTITY_ID_ATTRIBUTE, '');
$this->mode = $this->config->getValueValidate(self::MODE, self::KNOWN_MODES, self::MODE_PROXY);
$this->keepPerUser = $this->config->getIntegerRange(self::KEEP_PER_USER, 31, 1827, 31);
$this->requiredAuthSource = $this->config->getString(self::REQUIRE_AUTH_SOURCE, '');
$this->idAttribute = $this->config->getString(self::USER_ID_ATTRIBUTE, 'uid');
}
private function __clone()
{
}
public static function getInstance()
public static function getInstance(): self
{
if (null === self::$instance) {
self::$instance = new self();
......@@ -81,7 +94,7 @@ class Config
public function getIdAttribute()
{
return $this->config->getString(self::USER_ID_ATTRIBUTE, 'uid');
return $this->idAttribute;
}
public function getSourceIdpEntityIdAttribute()
......@@ -89,9 +102,11 @@ class Config
return $this->sourceIdpEntityIdAttribute;
}
public function getSideInfo($side)
public function getSideInfo(string $side)
{
assert(in_array($side, [self::SIDES], true));
if (!in_array($side, self::SIDES, true)) {
throw new \Exception('Unrecognized side parameter value passed \'' . $side . '\'.');
}
return array_merge([
'name' => '',
......@@ -101,11 +116,11 @@ class Config
public function getRequiredAuthSource()
{
return $this->config->getString(self::REQUIRE_AUTH_SOURCE, '');
return$this->requiredAuthSource;
}
public function getKeepPerUser()
{
return $this->config->getIntegerRange(self::KEEP_PER_USER, 31, 1827, 31);
return $this->keepPerUser;
}
}
......@@ -4,13 +4,16 @@ declare(strict_types=1);
namespace SimpleSAML\Module\proxystatistics;
use Exception;
use PDO;
use PDOStatement;
use SimpleSAML\Database;
use SimpleSAML\Logger;
class DatabaseCommand
{
public const TABLE_SUM = 'statistics_sums';
private const DEBUG_PREFIX = 'proxystatistics:DatabaseCommand - ';
private const TABLE_PER_USER = 'statistics_per_user';
......@@ -18,6 +21,10 @@ class DatabaseCommand
private const TABLE_SP = 'statistics_sp';
private const KEY_ID = 'id';
private const KEY_NAME = 'name';
private const TABLE_SIDES = [
Config::MODE_IDP => self::TABLE_IDP,
Config::MODE_SP => self::TABLE_SP,
......@@ -47,25 +54,26 @@ class DatabaseCommand
{
$this->config = Config::getInstance();
$this->conn = Database::getInstance($this->config->getStore());
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
$this->escape_char = '"';
} elseif ($this->isMysql()) {
$this->escape_char = '`';
} else {
$this->unknownDriver();
}
$this->tables = array_merge($this->tables, $this->config->getTables());
$this->mode = $this->config->getMode();
}
public static function prependColon($str)
{
return ':' . $str;
}
public function insertLogin(&$request, &$date)
public function insertLogin($request, &$date)
{
$entities = $this->getEntities($request);
$entities = $this->prepareEntitiesData($request);
foreach (Config::SIDES as $side) {
if (empty($entities[$side]['id'])) {
Logger::error('idpEntityId or spEntityId is empty and login log was not inserted into the database.');
if (empty($entities[$side][self::KEY_ID])) {
Logger::error(
self::DEBUG_PREFIX . 'idpEntityId or spEntityId is empty and login log was not inserted into the database.'
);
return;
}
......@@ -76,15 +84,19 @@ class DatabaseCommand
$ids = [];
foreach (self::TABLE_SIDES as $side => $table) {
$tableId = self::TABLE_IDS[$table];
$ids[$tableId] = $this->getIdFromIdentifier($table, $entities[$side], $tableId);
$ids[$tableId] = $this->getEntityDbIdFromEntityIdentifier($table, $entities[$side], $tableId);
}
if (false === $this->writeLogin($date, $ids, $userId)) {
Logger::error('The login log was not inserted.');
Logger::error(self::DEBUG_PREFIX . 'login record has not been inserted (data \'' . json_encode([
'user' => $userId,
'ids' => $ids,
'date' => $date,
]) . '\'.');
}
}
public function getNameById($side, $id)
public function getEntityNameByEntityIdentifier($side, $id)
{
$table = self::TABLE_SIDES[$side];
......@@ -101,10 +113,12 @@ class DatabaseCommand
public function getLoginCountPerDay($days, $where = [])
{
$params = [];
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
$query = "SELECT EXTRACT(epoch FROM TO_DATE(CONCAT(year,'-',month,'-',day), 'YYYY-MM-DD')) AS day, ";
} else {
} elseif ($this->isMysql()) {
$query = "SELECT UNIX_TIMESTAMP(STR_TO_DATE(CONCAT(year,'-',month,'-',day), '%Y-%m-%d')) AS day, ";
} else {
$this->unknownDriver();
}
$query .= 'logins AS count, users ' .
'FROM ' . $this->tables[self::TABLE_SUM] . ' ' .
......@@ -148,7 +162,7 @@ class DatabaseCommand
foreach ([self::TABLE_IDS[self::TABLE_SP], null] as $sp_id) {
$ids = [$idp_id, $sp_id];
$msg = 'Aggregating daily statistics per ' . implode(' and ', array_filter($ids));
Logger::info($msg);
Logger::info(self::DEBUG_PREFIX . $msg);
$query = 'INSERT INTO ' . $this->tables[self::TABLE_SUM] . ' '
. '(' . $this->escape_cols(['year', 'month', 'day', 'idp_id', 'sp_id', 'logins', 'users']) . ') '
. 'SELECT EXTRACT(YEAR FROM ' . $this->escape_col(
......@@ -163,16 +177,18 @@ class DatabaseCommand
. 'FROM ' . $this->tables[self::TABLE_PER_USER] . ' '
. 'WHERE day<DATE(NOW()) '
. 'GROUP BY ' . $this->getAggregateGroupBy($ids) . ' ';
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
$query .= 'ON CONFLICT (' . $this->escape_cols(
['year', 'month', 'day', 'idp_id', 'sp_id']
) . ') DO NOTHING;';
} else {
} elseif ($this->isMysql()) {
$query .= 'ON DUPLICATE KEY UPDATE id=id;';
} else {
$this->unknownDriver();
}
// do nothing if row already exists
if (!$this->conn->write($query)) {
Logger::warning($msg . ' failed');
Logger::warning(self::DEBUG_PREFIX . $msg . ' failed');
}
}
}
......@@ -180,12 +196,12 @@ class DatabaseCommand
$keepPerUserDays = $this->config->getKeepPerUser();
$msg = 'Deleting detailed statistics';
Logger::info($msg);
if ('pgsql' === $this->conn->getDriver()) {
Logger::info(self::DEBUG_PREFIX . $msg);
if ($this->isPgsql()) {
$make_date = 'MAKE_DATE(' . $this->escape_cols(['year', 'month', 'day']) . ')';
$date_clause = sprintf('CURRENT_DATE - INTERVAL \'%s DAY\' ', $keepPerUserDays);
$params = [];
} else {
} elseif ($this->isMysql()) {
$make_date = 'STR_TO_DATE(CONCAT(' . $this->escape_col('year') . ",'-'," . $this->escape_col(
'month'
) . ",'-'," . $this->escape_col('day') . "), '%Y-%m-%d')";
......@@ -193,6 +209,8 @@ class DatabaseCommand
$params = [
'days' => $keepPerUserDays,
];
} else {
$this->unknownDriver();
}
$query = 'DELETE FROM ' . $this->tables[self::TABLE_PER_USER] . ' WHERE ' . $this->escape_col(
'day'
......@@ -200,56 +218,33 @@ class DatabaseCommand
. ' AND ' . $this->escape_col(
'day'
) . ' IN (SELECT ' . $make_date . ' FROM ' . $this->tables[self::TABLE_SUM] . ')';
if (
!$this->conn->write($query, $params)
) {
Logger::warning($msg . ' failed');
$written = $this->conn->write($query, $params);
if (is_bool($written) && !$written) {
Logger::warning(self::DEBUG_PREFIX . $msg . ' failed');
} elseif (0 === $written) {
Logger::warning(self::DEBUG_PREFIX . $msg . ' completed, but updated 0 rows.');
} else {
Logger::info(self::DEBUG_PREFIX . $msg . ' completed and updated ' . $written . ' rows.');
}
}
private function escape_col($col_name)
public static function prependColon($str): string
{
return $this->escape_char . $col_name . $this->escape_char;
}
private function escape_cols($col_names)
{
return $this->escape_char . implode(
$this->escape_char . ',' . $this->escape_char,
$col_names
) . $this->escape_char;
return ':' . $str;
}
private function read($query, $params)
private function writeLogin($date, $ids, $user): bool
{
return $this->conn->read($query, $params);
}
if (empty($user)) {
Logger::warning(self::DEBUG_PREFIX . 'user is unknown, cannot insert login. Ending prematurely.');
private function addWhereId($where, &$query, &$params)
{
$parts = [];
foreach ($where as $side => $value) {
$table = self::TABLE_SIDES[$side];
$column = self::TABLE_IDS[$table];
$part = $column;
if (null === $value) {
$part .= '=0';
} else {
$part .= '=:id';
$params['id'] = $value;
}
$parts[] = $part;
}
if (empty($parts)) {
$parts[] = '1=1';
return false;
}
$query .= implode(' AND ', $parts);
$query .= ' ';
}
if (empty($ids[self::TABLE_IDS[self::TABLE_IDP]]) || empty($ids[self::TABLE_IDS[self::TABLE_SP]])) {
Logger::warning(
self::DEBUG_PREFIX . 'no IDP_ID or SP_ID has been provided, cannot insert login. Ending prematurely.'
);
private function writeLogin($date, $ids, $user)
{
if (empty($user)) {
return false;
}
$params = array_merge($ids, [
......@@ -260,59 +255,75 @@ class DatabaseCommand
$fields = array_keys($params);
$placeholders = array_map(['self', 'prependColon'], $fields);
$query = 'INSERT INTO ' . $this->tables[self::TABLE_PER_USER] . ' (' . $this->escape_cols($fields) . ')' .
' VALUES (' . implode(', ', $placeholders) . ') ';
if ('pgsql' === $this->conn->getDriver()) {
' VALUES (' . implode(', ', $placeholders) . ') ';
if ($this->isPgsql()) {
$query .= 'ON CONFLICT (' . $this->escape_cols(
['day', 'idp_id', 'sp_id', 'user']
) . ') DO UPDATE SET "logins" = ' . $this->tables[self::TABLE_PER_USER] . '.logins + 1;';
} else {
} elseif ($this->isMysql()) {
$query .= 'ON DUPLICATE KEY UPDATE logins = logins + 1;';
} else {
$this->unknownDriver();
}
$written = $this->conn->write($query, $params);
if (is_bool($written) && !$written) {
Logger::debug(self::DEBUG_PREFIX . 'login entry write has failed.');
return false;
}
if (0 === $written) {
Logger::debug(self::DEBUG_PREFIX . 'login entry has been inserted, but has updated 0 rows.');
return $this->conn->write($query, $params);
return false;
}
return true;
}
private function getEntities($request): array
private function prepareEntitiesData($request): array
{
$entities = [
Config::MODE_IDP => [],
Config::MODE_SP => [],
];
if (Config::MODE_IDP !== $this->mode && Config::MODE_MULTI_IDP !== $this->mode) {
$entities[Config::MODE_IDP]['id'] = $this->getIdpIdentifier($request);
$entities[Config::MODE_IDP]['name'] = $this->getIdpName($request);
$entities[Config::MODE_IDP][self::KEY_ID] = $this->getIdpIdentifier($request);
$entities[Config::MODE_IDP][self::KEY_NAME] =$this->getIdpName($request);
}
if (Config::MODE_SP !== $this->mode) {
$entities[Config::MODE_SP]['id'] = $this->getSpIdentifier($request);
$entities[Config::MODE_SP]['name'] = $this->getSpName($request);
$entities[Config::MODE_SP][self::KEY_ID] = $this->getSpIdentifier($request);
$entities[Config::MODE_SP][self::KEY_NAME] =$this->getSpName($request);
}
if (Config::MODE_PROXY !== $this->mode && Config::MODE_MULTI_IDP !== $this->mode) {
$entities[$this->mode] = $this->config->getSideInfo($this->mode);
if (empty($entities[$this->mode]['id']) || empty($entities[$this->mode]['name'])) {
Logger::error('Invalid configuration (id, name) for ' . $this->mode);
if (empty($entities[$this->mode][self::KEY_ID]) || empty($entities[$this->mode][self::KEY_NAME])) {
Logger::error(self::DEBUG_PREFIX . 'Invalid configuration (id, name) for ' . $this->mode);
}
}
if (Config::MODE_MULTI_IDP === $this->mode) {
$entities[Config::MODE_IDP] = $this->config->getSideInfo(Config::MODE_IDP);
if (empty($entities[Config::MODE_IDP]['id']) || empty($entities[Config::MODE_IDP]['name'])) {
Logger::error('Invalid configuration (id, name) for ' . $this->mode);
if (empty($entities[Config::MODE_IDP][self::KEY_ID]) || empty($entities[Config::MODE_IDP][self::KEY_NAME])) {
Logger::error(self::DEBUG_PREFIX . 'Invalid configuration (id, name) for ' . $this->mode);
}
}
return $entities;
}
private function getIdFromIdentifier($table, $entity, $idColumn)
private function getEntityDbIdFromEntityIdentifier($table, $entity, $idColumn)
{
$identifier = $entity['id'];
$name = $entity['name'];
$identifier = $entity[self::KEY_ID];
$name = $entity[self::KEY_NAME];
$query = 'INSERT INTO ' . $this->tables[$table] . '(identifier, name) VALUES (:identifier, :name1) ';
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
$query .= 'ON CONFLICT (identifier) DO UPDATE SET name = :name2;';
} else {
} else if ($this->isMysql()) {
$query .= 'ON DUPLICATE KEY UPDATE name = :name2';
} else {
$this->unknownDriver();
}
$this->conn->write($query, [
'identifier' => $identifier,
......@@ -323,9 +334,31 @@ class DatabaseCommand
return $this->read('SELECT ' . $idColumn . ' FROM ' . $this->tables[$table]
. ' WHERE identifier=:identifier', [
'identifier' => $identifier,
])
->fetchColumn()
;
])->fetchColumn();
}
// Query construction helper methods
private function addWhereId($where, &$query, &$params)
{
$parts = [];
foreach ($where as $side => $value) {
$table = self::TABLE_SIDES[$side];
$column = self::TABLE_IDS[$table];
$part = $column;
if (null === $value) {
$part .= '=0';
} else {
$part .= '=:id';
$params['id'] = $value;
}
$parts[] = $part;
}
if (empty($parts)) {
$parts[] = '1=1';
}
$query .= implode(' AND ', $parts);
$query .= ' ';
}
private function addDaysRange($days, &$query, &$params, $not = false)
......@@ -336,17 +369,19 @@ class DatabaseCommand
} else {
$query .= 'AND';
}
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
$query .= ' MAKE_DATE(year,month,day) ';
} else {
} elseif ($this->isMysql()) {
$query .= " CONCAT(year,'-',LPAD(month,2,'00'),'-',LPAD(day,2,'00')) ";
} else {
$this->unknownDriver();
}
if ($not) {
$query .= 'NOT ';
}
if ('pgsql' === $this->conn->getDriver()) {
if ($this->isPgsql()) {
if (!is_int($days) && !ctype_digit($days)) {
throw new \Exception('days have to be an integer');
throw new Exception('days have to be an integer');
}
$query .= sprintf('BETWEEN CURRENT_DATE - INTERVAL \'%s DAY\' AND CURRENT_DATE ', $days);
} else {
......@@ -356,7 +391,20 @@ class DatabaseCommand
}
}
private function getAggregateGroupBy($ids)
private function escape_col($col_name): string
{
return $this->escape_char . $col_name . $this->escape_char;
}
private function escape_cols($col_names): string
{
return $this->escape_char . implode(
$this->escape_char . ',' . $this->escape_char,
$col_names
) . $this->escape_char;
}
private function getAggregateGroupBy($ids): string
{
$columns = ['day'];
foreach ($ids as $id) {
......@@ -402,6 +450,27 @@ class DatabaseCommand
$displayName = $request['Destination']['name']['en'] ?? '';
}
return$displayName;
return $displayName;
}
private function read($query, $params): PDOStatement
{
return $this->conn->read($query, $params);
}
private function isPgsql(): bool
{
return 'pgsql' === $this->conn->getDriver();
}
private function isMysql(): bool
{
return 'mysql' === $this->conn->getDriver();
}
private function unknownDriver()
{
Logger::error(self::DEBUG_PREFIX . 'unsupported DB driver \'' . $this->conn->getDriver());
throw new Exception('Unsupported DB driver');
}
}
......@@ -10,6 +10,8 @@ use SimpleSAML\XHTML\Template;
class Templates
{
private const DEBUG_PREFIX = 'proxystatistics:Templates - ';
private const INSTANCE_NAME = 'instance_name';
public static function showProviders($side, $tab)
......@@ -95,7 +97,7 @@ class Templates
}
$t->data['head'] .= Utils::metaData('translations', $translations);
$name = $dbCmd->getNameById($side, $id);
$name = $dbCmd->getEntityNameByEntityIdentifier($side, $id);
$t->data['header'] = $t->t('{proxystatistics:stats:' . $side . 'Detail_header_name}') . $name;
$t->data['htmlinject']['htmlContentPost'][]
......@@ -148,7 +150,9 @@ class Templates
if (null !== $instanceName) {
$t->data['header'] = $instanceName . ' ' . $t->data['header'];
} else {
Logger::warning('Missing configuration: config.php - instance_name is not set.');
Logger::warning(
self::DEBUG_PREFIX . 'missing configuration option in config.php - instance_name is not set.'
);
}
self::headIncludes($t);
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment