diff --git a/CHANGELOG.md b/CHANGELOG.md index 42ba20178c3e67d8dc9526418712464d4a7d4416..8196e72e3063e10637d705d2d4c5d17d90d852f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ All notable changes to this project will be documented in this file. #### Added - Added configuration file for ESLint - Module now supports running statistics as IDP/SP +- Store detailed statistics(include some user identifier) for several days #### Changed - Using of short array syntax (from array() to []) diff --git a/config-templates/module_statisticsproxy.php b/config-templates/module_statisticsproxy.php index ce05dbdd441a95a7377ec4e892dc205e4175407e..1c9b653e2b4c74dca61cff97d869921b3f340f31 100644 --- a/config-templates/module_statisticsproxy.php +++ b/config-templates/module_statisticsproxy.php @@ -65,11 +65,29 @@ $config = [ */ ], + /* + * For how many days should detailed statistics (per user) be kept. + * @default 0 + */ + 'detailedDays' => 0, + + /** + * Which attribute should be used as user ID. + * @default uid + */ + 'userIdAttribute' => 'uid', + /* * Fill the table name for statistics */ 'statisticsTableName' => 'statisticsTableName', + /* + * Fill the table name for detailed statistics + * @default + */ + 'detailedStatisticsTableName' => 'statistics_detail', + /* * Fill the table name for identityProvidersMap */ diff --git a/config-templates/tables.sql b/config-templates/tables.sql index 94e218830c80421dd0277dd0726b5a34d669ce33..3f0a1b1a34eb388e3b56e7aa2ad13d18e15b1818 100644 --- a/config-templates/tables.sql +++ b/config-templates/tables.sql @@ -11,6 +11,19 @@ CREATE TABLE statistics ( PRIMARY KEY (year, month, day, sourceIdp, service) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +CREATE TABLE statistics_detail ( + year INT NOT NULL, + month INT NOT NULL, + day INT NOT NULL, + sourceIdp VARCHAR(255) NOT NULL, + service VARCHAR(255) NOT NULL, + user VARCHAR(255) NOT NULL, + count INT, + INDEX (sourceIdp), + INDEX (service), + PRIMARY KEY (year, month, day, sourceIdp, service, user) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + --Tables for mapping identifier to name CREATE TABLE identityProvidersMap( entityId VARCHAR(255) NOT NULL, diff --git a/hooks/hook_cron.php b/hooks/hook_cron.php new file mode 100644 index 0000000000000000000000000000000000000000..d2c2d3b41228cb75877dcbd5584347397ce78cc0 --- /dev/null +++ b/hooks/hook_cron.php @@ -0,0 +1,24 @@ +<?php + +/** + * Hook to run a cron job. + * + * @param array &$croninfo Output + * @return void + */ +function proxystatistics_hook_cron(&$croninfo) +{ + if ($croninfo['tag'] !== 'daily') { + \SimpleSAML\Logger::debug('cron [proxystatistics]: Skipping cron in cron tag ['.$croninfo['tag'].'] '); + return; + } + + \SimpleSAML\Logger::info('cron [proxystatistics]: Running cron in cron tag ['.$croninfo['tag'].'] '); + + try { + $dbCmd = new \SimpleSAML\Module\proxystatistics\Auth\Process\DatabaseCommand(); + $dbCmd->deleteOldDetailedStatistics(); + } catch (\Exception $e) { + $croninfo['summary'][] = 'Error during deleting old detailed statistics: '.$e->getMessage(); + } +} diff --git a/lib/Auth/Process/DatabaseCommand.php b/lib/Auth/Process/DatabaseCommand.php index 229c32accaedfee721c5a580a2d87311751db1c1..f10d797bbac3ae01997b2c96bd58dd74123c7bf8 100644 --- a/lib/Auth/Process/DatabaseCommand.php +++ b/lib/Auth/Process/DatabaseCommand.php @@ -14,6 +14,7 @@ class DatabaseCommand private $databaseConnector; private $conn; private $statisticsTableName; + private $detailedStatisticsTableName; private $identityProvidersMapTableName; private $serviceProvidersMapTableName; @@ -21,12 +22,41 @@ class DatabaseCommand { $this->databaseConnector = new DatabaseConnector(); $this->conn = $this->databaseConnector->getConnection(); - assert($this->conn != null); + assert($this->conn !== null); $this->statisticsTableName = $this->databaseConnector->getStatisticsTableName(); + $this->detailedStatisticsTableName = $this->databaseConnector->getDetailedStatisticsTableName(); $this->identityProvidersMapTableName = $this->databaseConnector->getIdentityProvidersMapTableName(); $this->serviceProvidersMapTableName = $this->databaseConnector->getServiceProvidersMapTableName(); } + private function writeLogin($year, $month, $day, $sourceIdp, $service, $user = null) + { + $params = [ + 'year' => $year, + 'month' => $month, + 'day' => $day, + 'sourceIdp' => $sourceIdp, + 'service' => $service, + 'count' => 1, + ]; + $table = $this->statisticsTableName; + if ($user && $this->databaseConnector->getDetailedDays() > 0) { + // write also into aggregated statistics + self::writeLogin($year, $month, $day, $sourceIdp, $service); + $params['user'] = $user; + $table = $this->detailedStatisticsTableName; + } + $fields = array_keys($params); + $placeholders = array_map(function ($field) { + return ':' . $field; + + }, $fields); + $query = "INSERT INTO " . $table . " (" . implode(', ', $fields) . ")" . + " VALUES (" . implode(', ', $placeholders) . ") ON DUPLICATE KEY UPDATE count = count + 1"; + + return $this->conn->write($query, $params); + } + public function insertLogin(&$request, &$date) { if (!in_array($this->databaseConnector->getMode(), ['PROXY', 'IDP', 'SP'])) { @@ -59,11 +89,9 @@ class DatabaseCommand " is empty and login log wasn't inserted into the database." ); } else { - if ($this->conn->write( - "INSERT INTO " . $this->statisticsTableName . "(year, month, day, sourceIdp, service, count)" . - " VALUES (:year, :month, :day, :idp, :sp, '1') ON DUPLICATE KEY UPDATE count = count + 1", - ['year'=>$year, 'month'=>$month, 'day'=>$day, 'idp'=>$idpEntityID, 'sp'=>$spEntityId] - ) === false) { + $idAttribute = $this->databaseConnector->getUserIdAttribute(); + $userId = isset($request['Attributes'][$idAttribute]) ? $request['Attributes'][$idAttribute][0] : null; + if ($this->writeLogin($year, $month, $day, $idpEntityID, $spEntityId, $userId) === false) { Logger::error("The login log wasn't inserted into table: " . $this->statisticsTableName . "."); } @@ -197,7 +225,7 @@ class DatabaseCommand return $this->conn->read($query, $params)->fetchAll(PDO::FETCH_NUM); } - private static function addDaysRange($days, &$query, &$params) + private static function addDaysRange($days, &$query, &$params, $not = false) { if ($days != 0) { // 0 = all time if (stripos($query, "WHERE") === false) { @@ -205,9 +233,22 @@ class DatabaseCommand } else { $query .= "AND"; } - $query .= " CONCAT(year,'-',LPAD(month,2,'00'),'-',LPAD(day,2,'00')) " . - "BETWEEN CURDATE() - INTERVAL :days DAY AND CURDATE() "; + $query .= " CONCAT(year,'-',LPAD(month,2,'00'),'-',LPAD(day,2,'00')) "; + if ($not) { + $query .= "NOT "; + } + $query .= "BETWEEN CURDATE() - INTERVAL :days DAY AND CURDATE() "; $params['days'] = $days; } } + + public function deleteOldDetailedStatistics() + { + if ($this->databaseConnector->getDetailedDays() > 0) { + $query = "DELETE FROM " . $this->detailedStatisticsTableName . " "; + $params = []; + self::addDaysRange($this->databaseConnector->getDetailedDays(), $query, $params, true); + return $this->conn->write($query, $params); + } + } } diff --git a/lib/Auth/Process/DatabaseConnector.php b/lib/Auth/Process/DatabaseConnector.php index 1ffa89134add3b74ebbbe980afaf0b5dd08fee6b..34ada8b76c28b13c762f7df174d56f73882fee96 100644 --- a/lib/Auth/Process/DatabaseConnector.php +++ b/lib/Auth/Process/DatabaseConnector.php @@ -14,6 +14,7 @@ use PDO; class DatabaseConnector { private $statisticsTableName; + private $detailedStatisticsTableName; private $identityProvidersMapTableName; private $serviceProvidersMapTableName; private $mode; @@ -21,6 +22,8 @@ class DatabaseConnector private $idpName; private $spEntityId; private $spName; + private $detailedDays; + private $userIdAttribute; private $conn = null; const CONFIG_FILE_NAME = 'module_statisticsproxy.php'; @@ -35,6 +38,7 @@ class DatabaseConnector /** @deprecated */ const DATABASE = 'databaseName'; const STATS_TABLE_NAME = 'statisticsTableName'; + const DETAILED_STATS_TABLE_NAME = 'detailedStatisticsTableName'; const IDP_MAP_TABLE_NAME = 'identityProvidersMapTableName'; const SP_MAP_TABLE_NAME = 'serviceProvidersMapTableName'; /** @deprecated */ @@ -53,6 +57,8 @@ class DatabaseConnector const IDP_NAME = 'idpName'; const SP_ENTITY_ID = 'spEntityId'; const SP_NAME = 'spName'; + const DETAILED_DAYS = 'detailedDays'; + const USER_ID_ATTRIBUTE = 'userIdAttribute'; public function __construct() { @@ -87,6 +93,7 @@ class DatabaseConnector $this->storeConfig = Configuration::loadFromArray($this->storeConfig); $this->statisticsTableName = $conf->getString(self::STATS_TABLE_NAME); + $this->detailedStatisticsTableName = $conf->getString(self::DETAILED_STATS_TABLE_NAME, 'statistics_detail'); $this->identityProvidersMapTableName = $conf->getString(self::IDP_MAP_TABLE_NAME); $this->serviceProvidersMapTableName = $conf->getString(self::SP_MAP_TABLE_NAME); $this->mode = $conf->getString(self::MODE, 'PROXY'); @@ -94,6 +101,8 @@ class DatabaseConnector $this->idpName = $conf->getString(self::IDP_NAME, ''); $this->spEntityId = $conf->getString(self::SP_ENTITY_ID, ''); $this->spName = $conf->getString(self::SP_NAME, ''); + $this->detailedDays = $conf->getInteger(self::DETAILED_DAYS, 0); + $this->userIdAttribute = $conf->getString(self::USER_ID_ATTRIBUTE, 'uid'); } public function getConnection() @@ -106,6 +115,11 @@ class DatabaseConnector return $this->statisticsTableName; } + public function getDetailedStatisticsTableName() + { + return $this->detailedStatisticsTableName; + } + public function getIdentityProvidersMapTableName() { return $this->identityProvidersMapTableName; @@ -140,4 +154,14 @@ class DatabaseConnector { return $this->spName; } + + public function getDetailedDays() + { + return $this->detailedDays; + } + + public function getUserIdAttribute() + { + return $this->userIdAttribute; + } }