diff --git a/composer.json b/composer.json index c4d0c7d0532da83cbf1a8e1e8f407ba2ea4d5798..83694b451e6ae01a92768caa8d8f5c56f337afc1 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "ext-filter": "*", "ext-json": "*", "ext-PDO": "*", + "ext-curl": "*", "simplesamlphp/simplesamlphp": "^1.19.2" }, "suggest": { diff --git a/composer.lock b/composer.lock index 4eefa8ea72d07e09f2ec5d2886d86676301696f0..9d362fe98dfba0ee2ef18cb0bccc452d85a8c7dc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "07339668d056596b07150da71952359f", + "content-hash": "62a4708962123e4125638eec26fad0f3", "packages": [ { "name": "gettext/gettext", @@ -2608,16 +2608,16 @@ }, { "name": "symfony/console", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "90f21e27d0d88ce38720556dd164d4a1e4c3934c" + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/90f21e27d0d88ce38720556dd164d4a1e4c3934c", - "reference": "90f21e27d0d88ce38720556dd164d4a1e4c3934c", + "url": "https://api.github.com/repos/symfony/console/zipball/560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", + "reference": "560fc3ed7a43e6d30ea94a07d77f9a60b8ed0fb8", "shasum": "" }, "require": { @@ -2687,7 +2687,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.4.23" + "source": "https://github.com/symfony/console/tree/v5.4.24" }, "funding": [ { @@ -2703,20 +2703,20 @@ "type": "tidelift" } ], - "time": "2023-04-24T18:47:29+00:00" + "time": "2023-05-26T05:13:16+00:00" }, { "name": "symfony/dependency-injection", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "bb7b7988c898c94f5338e16403c52b5a3cae1d93" + "reference": "4645e032d0963fb614969398ca28e47605b1a7da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/bb7b7988c898c94f5338e16403c52b5a3cae1d93", - "reference": "bb7b7988c898c94f5338e16403c52b5a3cae1d93", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4645e032d0963fb614969398ca28e47605b1a7da", + "reference": "4645e032d0963fb614969398ca28e47605b1a7da", "shasum": "" }, "require": { @@ -2776,7 +2776,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v5.4.23" + "source": "https://github.com/symfony/dependency-injection/tree/v5.4.24" }, "funding": [ { @@ -2792,7 +2792,7 @@ "type": "tidelift" } ], - "time": "2023-04-21T15:04:16+00:00" + "time": "2023-05-05T14:42:55+00:00" }, { "name": "symfony/deprecation-contracts", @@ -2863,16 +2863,16 @@ }, { "name": "symfony/error-handler", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "218206b4772d9f412d7d277980c020d06e9d8a4e" + "reference": "c1b9be3b8a6f60f720bec28c4ffb6fb5b00a8946" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/218206b4772d9f412d7d277980c020d06e9d8a4e", - "reference": "218206b4772d9f412d7d277980c020d06e9d8a4e", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/c1b9be3b8a6f60f720bec28c4ffb6fb5b00a8946", + "reference": "c1b9be3b8a6f60f720bec28c4ffb6fb5b00a8946", "shasum": "" }, "require": { @@ -2914,7 +2914,7 @@ "description": "Provides tools to manage errors and ease debugging PHP code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/error-handler/tree/v5.4.23" + "source": "https://github.com/symfony/error-handler/tree/v5.4.24" }, "funding": [ { @@ -2930,7 +2930,7 @@ "type": "tidelift" } ], - "time": "2023-04-17T10:03:27+00:00" + "time": "2023-05-02T16:13:31+00:00" }, { "name": "symfony/event-dispatcher", @@ -3225,16 +3225,16 @@ }, { "name": "symfony/framework-bundle", - "version": "v5.4.22", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/framework-bundle.git", - "reference": "6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1" + "reference": "c06a56a47817d29318aaace1c655cbde16c998e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1", - "reference": "6cb4f6aed4bd7fbf7b2ee74c231184a07f3d00c1", + "url": "https://api.github.com/repos/symfony/framework-bundle/zipball/c06a56a47817d29318aaace1c655cbde16c998e8", + "reference": "c06a56a47817d29318aaace1c655cbde16c998e8", "shasum": "" }, "require": { @@ -3248,7 +3248,7 @@ "symfony/event-dispatcher": "^5.1|^6.0", "symfony/filesystem": "^4.4|^5.0|^6.0", "symfony/finder": "^4.4|^5.0|^6.0", - "symfony/http-foundation": "^5.3|^6.0", + "symfony/http-foundation": "^5.4.24|^6.2.11", "symfony/http-kernel": "^5.4|^6.0", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php80": "^1.16", @@ -3261,7 +3261,6 @@ "doctrine/persistence": "<1.3", "phpdocumentor/reflection-docblock": "<3.2.2", "phpdocumentor/type-resolver": "<1.4.0", - "phpunit/phpunit": "<5.4.3", "symfony/asset": "<5.3", "symfony/console": "<5.2.5", "symfony/dom-crawler": "<4.4", @@ -3356,7 +3355,7 @@ "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/framework-bundle/tree/v5.4.22" + "source": "https://github.com/symfony/framework-bundle/tree/v5.4.24" }, "funding": [ { @@ -3372,20 +3371,20 @@ "type": "tidelift" } ], - "time": "2023-03-31T08:25:44+00:00" + "time": "2023-05-25T13:05:00+00:00" }, { "name": "symfony/http-foundation", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "af9fbb378f5f956c8f29d4886644c84c193780ac" + "reference": "3c59f97f6249ce552a44f01b93bfcbd786a954f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af9fbb378f5f956c8f29d4886644c84c193780ac", - "reference": "af9fbb378f5f956c8f29d4886644c84c193780ac", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3c59f97f6249ce552a44f01b93bfcbd786a954f5", + "reference": "3c59f97f6249ce552a44f01b93bfcbd786a954f5", "shasum": "" }, "require": { @@ -3432,7 +3431,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v5.4.23" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.24" }, "funding": [ { @@ -3448,20 +3447,20 @@ "type": "tidelift" } ], - "time": "2023-04-18T06:30:11+00:00" + "time": "2023-05-19T07:21:23+00:00" }, { "name": "symfony/http-kernel", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "48ea17a7c65ef1ede0c3b2dbc35adace99071810" + "reference": "f38b722e1557eb3f487d351b48f5a1279b50e9d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/48ea17a7c65ef1ede0c3b2dbc35adace99071810", - "reference": "48ea17a7c65ef1ede0c3b2dbc35adace99071810", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/f38b722e1557eb3f487d351b48f5a1279b50e9d1", + "reference": "f38b722e1557eb3f487d351b48f5a1279b50e9d1", "shasum": "" }, "require": { @@ -3544,7 +3543,7 @@ "description": "Provides a structured process for converting a Request into a Response", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-kernel/tree/v5.4.23" + "source": "https://github.com/symfony/http-kernel/tree/v5.4.24" }, "funding": [ { @@ -3560,7 +3559,7 @@ "type": "tidelift" } ], - "time": "2023-04-28T13:29:52+00:00" + "time": "2023-05-27T08:06:30+00:00" }, { "name": "symfony/polyfill-ctype", @@ -4470,16 +4469,16 @@ }, { "name": "symfony/var-dumper", - "version": "v5.4.23", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "9a8a5b6d6508928174ded2109e29328a55342a42" + "reference": "8e12706bf9c68a2da633f23bfdc15b4dce5970b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/9a8a5b6d6508928174ded2109e29328a55342a42", - "reference": "9a8a5b6d6508928174ded2109e29328a55342a42", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/8e12706bf9c68a2da633f23bfdc15b4dce5970b3", + "reference": "8e12706bf9c68a2da633f23bfdc15b4dce5970b3", "shasum": "" }, "require": { @@ -4488,7 +4487,6 @@ "symfony/polyfill-php80": "^1.16" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<4.4" }, "require-dev": { @@ -4539,7 +4537,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v5.4.23" + "source": "https://github.com/symfony/var-dumper/tree/v5.4.24" }, "funding": [ { @@ -4555,7 +4553,7 @@ "type": "tidelift" } ], - "time": "2023-04-18T09:26:27+00:00" + "time": "2023-05-25T13:05:00+00:00" }, { "name": "symfony/var-exporter", @@ -4963,7 +4961,8 @@ "ext-ctype": "*", "ext-filter": "*", "ext-json": "*", - "ext-pdo": "*" + "ext-pdo": "*", + "ext-curl": "*" }, "platform-dev": [], "platform-overrides": { diff --git a/lib/Auth/Process/ApiStatistics.php b/lib/Auth/Process/ApiStatistics.php new file mode 100644 index 0000000000000000000000000000000000000000..4a4fab17bfea7bc7db00b2f3d682363466b26b88 --- /dev/null +++ b/lib/Auth/Process/ApiStatistics.php @@ -0,0 +1,139 @@ +<?php + +declare(strict_types=1); + +namespace SimpleSAML\Module\proxystatistics\Auth\Process; + +use SimpleSAML\Auth\ProcessingFilter; +use SimpleSAML\Configuration; +use SimpleSAML\Error\Exception; +use SimpleSAML\Logger; + +class ApiStatistics extends ProcessingFilter +{ + private const STAGE = 'proxystatistics:ApiStatistics'; + + private const DEBUG_PREFIX = self::STAGE . ' - '; + + public const API_URL = 'apiUrl'; + + public const API_USERNAME = 'apiUsername'; + + public const API_PASSWORD = 'apiPassword'; + + public const USERNAME_ATTRIBUTE = 'usernameAttribute'; + + public const IDP_IDENTIFIER_ATTRIBUTE = 'idpIdentifierAttribute'; + + public const IDP_NAME_ATTRIBUTE = 'idpNameAttribute'; + + private const USER_ID = 'userId'; + + private const SERVICE_IDENTIFIER = 'serviceIdentifier'; + + private const SERVICE_NAME = 'serviceName'; + + private const IDP_IDENTIFIER = 'idpIdentifier'; + + private const IDP_NAME = 'idpName'; + + private $userIdAttribute; + + private $idpIdAttribute; + + private $idpNameAttribute; + + private $apiUrl; + + private $apiUsername; + + private $apiPassword; + + public function __construct($config, $reserved) + { + parent::__construct($config, $reserved); + $configuration = Configuration::loadFromArray($config); + $this->userIdAttribute = $configuration->getString(self::USERNAME_ATTRIBUTE); + $this->idpIdAttribute = $configuration->getString(self::IDP_IDENTIFIER_ATTRIBUTE); + $this->idpNameAttribute = $configuration->getString(self::IDP_NAME_ATTRIBUTE); + $this->apiUrl = $configuration->getString(self::API_URL); + $this->apiUsername = $configuration->getString(self::API_USERNAME); + $this->apiPassword = $configuration->getString(self::API_PASSWORD); + } + + public function process(&$request) + { + $attributes = $request['Attributes']; + + if (empty($attributes[$this->userIdAttribute][0])) { + Logger::warning( + self::DEBUG_PREFIX . + "Cannot write to stats - Missing user ID in attribute " . + $this->userIdAttribute + ); + return; + } elseif (empty($attributes[$this->idpIdAttribute][0])) { + Logger::warning( + self::DEBUG_PREFIX . + "Cannot write to stats - Missing IdP ID in attribute " . + $this->idpIdAttribute + ); + return; + } elseif (empty($attributes[$this->idpNameAttribute][0])) { + Logger::warning( + self::DEBUG_PREFIX . + "Cannot write to stats - Missing IdP name in attribute " . + $this->idpNameAttribute + ); + } + $userId = $attributes[$this->userIdAttribute][0]; + $idpId = $attributes[$this->idpIdAttribute][0]; + $idpName = $attributes[$this->idpNameAttribute][0]; + + $spId = $request['Destination']['entityid']; + $spName = $request['Destination']['UIInfo']['DisplayName']['en'] ?? ''; + if (empty($spName)) { + $spName = $request['Destination']['name']['en'] ?? ''; + } + + if (empty($spId)) { + Logger::warning(self::DEBUG_PREFIX . "Cannot write to stats - Missing SP ID"); + return; + } elseif (empty($spName)) { + Logger::warning(self::DEBUG_PREFIX . "Cannot write to stats - Missing SP name"); + return; + } + + $body = json_encode([ + self::USER_ID => $userId, + self::IDP_IDENTIFIER => $idpId, + self::IDP_NAME => $idpName, + self::SERVICE_IDENTIFIER => $spId, + self::SERVICE_NAME => $spName + ]); + + Logger::debug(self::DEBUG_PREFIX . "Calling stats API endpoint with data - " . $body); + + $ch = curl_init($this->apiUrl); + curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type:application/json']); + curl_setopt($ch, CURLOPT_HEADER, 1); + curl_setopt($ch, CURLOPT_TIMEOUT, 30); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + curl_setopt($ch, CURLOPT_USERPWD, $this->apiUsername . ":" . $this->apiPassword); + + $response = curl_exec($ch); + $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + if ($httpcode != 200) { + Logger::warning( + self::DEBUG_PREFIX + . 'Calling API endpoint to write stats returned non-200 response code: ' . $httpcode + ); + Logger::debug(self::DEBUG_PREFIX . "Response: " . $response); + } else { + Logger::info(self::DEBUG_PREFIX . 'Successfully written to statistics via API'); + } + } +}