diff --git a/bin/test.php b/bin/test.php index 8880791b1c313e6879e39345c7f32402475a0ceb..cf32aff522b2369015f7b4d50e3a40ccb8406181 100644 --- a/bin/test.php +++ b/bin/test.php @@ -1,6 +1,6 @@ #!/usr/bin/env php <?php -// TODO mivanci remove this file +// TODO mivanci remove this file before release declare(strict_types=1); use SimpleSAML\Module\accounting\Entities\Authentication\Event; @@ -34,7 +34,7 @@ echo $newLine; $spinnerChars = ['|', '/', '-', '\\']; -/** +/**/ echo 'Starting simulating MySQL: '; $mysqlStartTime = new DateTime(); echo $mysqlStartTime->format(DateTime::ATOM); @@ -87,7 +87,7 @@ for ($i = 1; $i <= $numberOfItems; $i++) { echo $newLine; echo $newLine; -*/ + echo 'Starting simulating Redis: '; $redisStartTime = new DateTime(); echo $redisStartTime->format(DateTime::ATOM); diff --git a/config-templates/module_accounting.php b/config-templates/module_accounting.php index 62e9a4ec0c68a629b1e885e0e4c96a506073afba..87aa3f61eed001ccb648e88583581dc62468095c 100644 --- a/config-templates/module_accounting.php +++ b/config-templates/module_accounting.php @@ -40,14 +40,6 @@ $config = [ */ //ModuleConfiguration\AccountingProcessingType::VALUE_ASYNCHRONOUS, - /** - * Cron tags. - * - * Job runner tag designates the cron tag to use when running accounting jobs. Make sure to add this tag to - * the cron module configuration in case of the 'asynchronous' accounting processing type. - */ - ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner', - /** * Jobs store class. In case of the 'asynchronous' accounting processing type, this determines which class * will be used to store jobs. The class must implement Stores\Interfaces\JobsStoreInterface. @@ -79,14 +71,12 @@ $config = [ */ Trackers\Authentication\DoctrineDbal\Versioned\Tracker::class, - /** * Additional trackers to run besides default data tracker. These trackers will typically only process and * persist authentication data to proper data store, and won't be used to display data in SSP UI. * These tracker classes must implement Trackers\Interfaces\AuthenticationDataTrackerInterface. */ ModuleConfiguration::OPTION_ADDITIONAL_TRACKERS => [ - // TODO mivanci at least one more tracker and its connection // tracker-class ], @@ -164,17 +154,13 @@ $config = [ /** * Job runner fine-grained configuration options. - */ - - /** - * Maximum execution time for the job runner. * - * You can use this option to limit job runner activity by combining when the job runner will run (using - * cron configuration) and how long the job runner will be active (execution time). This can be null, - * meaning it will run indefinitely, or can be set as a duration for DateInterval, examples being - * below. Note that when the job runner is run using Cron user interface in SimpleSAMLphp, the - * duration will be taken from the 'max_execution_time' ini setting, and will override this - * setting if ini setting is shorter. + * Maximum execution time for the job runner. You can use this option to limit job runner activity by combining + * when the job runner will run (using cron configuration) and how long the job runner will be active + * (execution time). This can be null, meaning it will run indefinitely, or can be set as a duration + * for DateInterval, examples being below. Note that when the job runner is run using Cron user + * interface in SimpleSAMLphp, the duration will be taken from the 'max_execution_time' ini + * setting, and will override this setting if ini setting is shorter. * @see https://www.php.net/manual/en/dateinterval.construct.php */ ModuleConfiguration::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME => null, @@ -189,4 +175,32 @@ $config = [ * backend store. If the value is null, there will be no pause. */ ModuleConfiguration::OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED => 10, + + /** + * Tracker data retention policy. + * + * Determines how long the tracked data will be stored. If null, data will be stored indefinitely. Otherwise, it + * can be set as a duration for DateInterval, examples being below. For this to work, a cron tag must also + * be configured. + */ + ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => null, + //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P30D', // 30 days + //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P6M', // 6 months + //ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P1Y', // 1 year + + + /** + * Cron tags. + * + * Job runner tag designates the cron tag to use when running accounting jobs. Make sure to add this tag to + * the cron module configuration in case of the 'asynchronous' accounting processing type. + */ + ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner', + + /** + * Tracker data retention policy tag designates the cron tag to use for enforcing data retention policy. Make sure + * to add this tag to the cron module configuration if data retention policy is different to null. + */ + ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY => + 'accounting_tracker_data_retention_policy', ]; diff --git a/hooks/hook_adminmenu.php b/hooks/hook_adminmenu.php index edfee988240a4634c1325c7456fc63a684059284..cb7b83be42f8f3b54cd341bbbe3fd8618cd29827 100644 --- a/hooks/hook_adminmenu.php +++ b/hooks/hook_adminmenu.php @@ -19,6 +19,10 @@ function accounting_hook_adminmenu(\SimpleSAML\XHTML\Template &$template): void ], ]; + if (!isset($template->data[$menuKey]) || !is_array($template->data[$menuKey])) { + return; + } + // Use array_splice to put our entry before the "Log out" entry. array_splice($template->data[$menuKey], -1, 0, $profilePageEntry); diff --git a/hooks/hook_configpage.php b/hooks/hook_configpage.php index 9b103adc7736fd19a54545dbf798cc335981f97c..daff38442d0f48a95e6e3e0ae2f5ae3c97ad8f96 100644 --- a/hooks/hook_configpage.php +++ b/hooks/hook_configpage.php @@ -11,7 +11,13 @@ function accounting_hook_configpage(Template &$template): void { $moduleRoutesHelper = new ModuleRoutesHelper(); - $template->data['links'][] = [ + $dataLinksKey = 'links'; + + if (!isset($template->data[$dataLinksKey]) || !is_array($template->data[$dataLinksKey])) { + return; + } + + $template->data[$dataLinksKey][] = [ 'href' => $moduleRoutesHelper->getUrl(ModuleRoutesHelper::PATH_ADMIN_CONFIGURATION_STATUS), 'text' => Translate::noop('Profile Page configuration status'), ]; diff --git a/hooks/hook_cron.php b/hooks/hook_cron.php index fbc57163510c4c1ba486893958e20c4bcf76efe0..ee6c7c2203a19a3cc82f90b1b93c357109494b4a 100644 --- a/hooks/hook_cron.php +++ b/hooks/hook_cron.php @@ -2,20 +2,33 @@ declare(strict_types=1); +use Psr\Log\LoggerInterface; +use SimpleSAML\Configuration; +use SimpleSAML\Module\accounting\Services\HelpersManager; use SimpleSAML\Module\accounting\Services\JobRunner; use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Services\Logger; +use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder; function accounting_hook_cron(array &$cronInfo): void { $moduleConfiguration = new ModuleConfiguration(); + $logger = new Logger(); + /** @var ?string $currentCronTag */ $currentCronTag = $cronInfo['tag'] ?? null; - $cronTagForJobRunner = $moduleConfiguration->getCronTagForJobRunner(); + if (!isset($cronInfo['summary']) || !is_array($cronInfo['summary'])) { + $cronInfo['summary'] = []; + } + /** + * Job runner handling. + */ + $cronTagForJobRunner = $moduleConfiguration->getCronTagForJobRunner(); try { if ($currentCronTag === $cronTagForJobRunner) { - $state = (new JobRunner($moduleConfiguration, \SimpleSAML\Configuration::getConfig()))->run(); + $state = (new JobRunner($moduleConfiguration, Configuration::getConfig()))->run(); foreach ($state->getStatusMessages() as $statusMessage) { $cronInfo['summary'][] = $statusMessage; } @@ -31,4 +44,48 @@ function accounting_hook_cron(array &$cronInfo): void $message = 'Job runner error: ' . $exception->getMessage(); $cronInfo['summary'][] = $message; } + + if (!isset($cronInfo['summary']) || !is_array($cronInfo['summary'])) { + $cronInfo['summary'] = []; + } + + /** + * Tracker data retention policy handling. + */ + $cronTagForTrackerDataRetentionPolicy = $moduleConfiguration->getCronTagForTrackerDataRetentionPolicy(); + try { + if ( + $currentCronTag === $cronTagForTrackerDataRetentionPolicy && + ($retentionPolicy = $moduleConfiguration->getTrackerDataRetentionPolicy()) !== null + ) { + $helpersManager = new HelpersManager(); + $message = sprintf('Handling data retention policy.'); + $logger->info($message); + $cronInfo['summary'][] = $message; + handleDataRetentionPolicy($moduleConfiguration, $logger, $helpersManager, $retentionPolicy); + } + } catch (Throwable $exception) { + $message = 'Error enforcing tracker data retention policy: ' . $exception->getMessage(); + $cronInfo['summary'][] = $message; + } +} + +function handleDataRetentionPolicy( + ModuleConfiguration $moduleConfiguration, + LoggerInterface $logger, + HelpersManager $helpersManager, + DateInterval $retentionPolicy +): void { + // Handle default data tracker and provider + (new AuthenticationDataTrackerBuilder($moduleConfiguration, $logger, $helpersManager)) + ->build($moduleConfiguration->getDefaultDataTrackerAndProviderClass()) + ->enforceDataRetentionPolicy($retentionPolicy); + + $additionalTrackers = $moduleConfiguration->getAdditionalTrackers(); + + foreach ($additionalTrackers as $tracker) { + (new AuthenticationDataTrackerBuilder($moduleConfiguration, $logger, $helpersManager)) + ->build($tracker) + ->enforceDataRetentionPolicy($retentionPolicy); + } } \ No newline at end of file diff --git a/psalm.xml b/psalm.xml index 87c90075a861695c2d56c923737061571b81524e..ccad21aa75b84382afc60f916d787ce5105425c1 100644 --- a/psalm.xml +++ b/psalm.xml @@ -11,6 +11,7 @@ <directory name="config-templates" /> <directory name="tests" /> <directory name="www" /> + <directory name="hooks" /> <ignoreFiles> <directory name="vendor" /> diff --git a/routing/routes/routes.yml b/routing/routes/routes.yml index c6bc1a186a23913a851a3aa3e93af975bc7e2ea0..138cde0773523be99175e3bc371a80168237d921 100644 --- a/routing/routes/routes.yml +++ b/routing/routes/routes.yml @@ -1,12 +1,8 @@ -accounting-setup: - path: /setup - defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::setup' } - -# TODO mivanci delete -accounting-job-runner: - path: /job-runner - defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::jobRunner' } +# TODO mivanci delete test route +accounting-test: + path: /test + defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Test::test' } accounting-admin-configuration-status: path: /admin/configuration/status diff --git a/src/Auth/Process/Accounting.php b/src/Auth/Process/Accounting.php index a2488982fae8cc4911737a18f8f73005a6547344..817fa9e6636cb0e87eb86b9b1e8291f4c103dfac 100644 --- a/src/Auth/Process/Accounting.php +++ b/src/Auth/Process/Accounting.php @@ -72,7 +72,6 @@ class Accounting extends ProcessingFilter $this->moduleConfiguration->getAdditionalTrackers() ); - /** @var string $tracker */ foreach ($configuredTrackers as $tracker) { ($this->authenticationDataTrackerBuilder->build($tracker))->process($authenticationEvent); } diff --git a/src/Helpers/RandomHelper.php b/src/Helpers/RandomHelper.php index 30256837765db4ebe4806e951d31ae66974af799..3a74a7fe318a6705c8835bc10a2ae02c72e90d63 100644 --- a/src/Helpers/RandomHelper.php +++ b/src/Helpers/RandomHelper.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Helpers; class RandomHelper @@ -8,8 +10,10 @@ class RandomHelper { try { return random_int($minimum, $maximum); + // @codeCoverageIgnoreStart } catch (\Throwable $exception) { return mt_rand($minimum, $maximum); + // @codeCoverageIgnoreEnd } } } diff --git a/src/Http/Controllers/Admin/Configuration.php b/src/Http/Controllers/Admin/Configuration.php index 3f585d1e1adb006f08f0801d243eca2c981dae07..909518ce4e4dbdf56fc72f881c67870c7546c668 100644 --- a/src/Http/Controllers/Admin/Configuration.php +++ b/src/Http/Controllers/Admin/Configuration.php @@ -55,6 +55,7 @@ class Configuration $configurationValidationErrors = null; $jobsStore = null; $defaultDataTrackerAndProvider = null; + $additionalTrackers = []; $setupNeeded = false; $runSetup = $request->query->has('runSetup'); @@ -87,7 +88,21 @@ class Configuration } } } - // TODO mivanci setup for additional trackers from configuration + + foreach ($moduleConfiguration->getAdditionalTrackers() as $trackerClass) { + $additionalTrackerInstance = + (new AuthenticationDataTrackerBuilder($moduleConfiguration, $this->logger, $this->helpersManager)) + ->build($trackerClass); + + if ($additionalTrackerInstance->needsSetup()) { + if ($runSetup) { + $additionalTrackerInstance->runSetup(); + } else { + $setupNeeded = true; + } + } + $additionalTrackers[$trackerClass] = $additionalTrackerInstance; + } } catch (Throwable $exception) { $configurationValidationErrors = $exception->getMessage(); } @@ -97,6 +112,7 @@ class Configuration 'configurationValidationErrors' => $configurationValidationErrors, 'jobsStore' => $jobsStore, 'defaultDataTrackerAndProvider' => $defaultDataTrackerAndProvider, + 'additionalTrackers' => $additionalTrackers, 'setupNeeded' => $setupNeeded, 'profilePageUri' => $this->helpersManager->getModuleRoutesHelper() ->getUrl(ModuleRoutesHelper::PATH_USER_PERSONAL_DATA), diff --git a/src/Http/Controllers/Test.php b/src/Http/Controllers/Test.php index 697a50ad3c06ba935a4637c47148fb33f21bd8a3..e7b4446d6942869d0d30d92e60655bef7c047ae5 100644 --- a/src/Http/Controllers/Test.php +++ b/src/Http/Controllers/Test.php @@ -12,6 +12,7 @@ use SimpleSAML\Locale\Translate; use SimpleSAML\Module\accounting\Entities\Authentication\Event; use SimpleSAML\Module\accounting\Entities\Authentication\State; use SimpleSAML\Module\accounting\ModuleConfiguration; +use SimpleSAML\Module\accounting\Services\HelpersManager; use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder; use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; use SimpleSAML\Module\accounting\Trackers\Authentication\DoctrineDbal\Versioned\Tracker; @@ -30,6 +31,7 @@ class Test protected Session $session; protected ModuleConfiguration $moduleConfiguration; protected LoggerInterface $logger; + protected HelpersManager $helpersManager; /** * @param SspConfiguration $sspConfiguration @@ -41,12 +43,14 @@ class Test SspConfiguration $sspConfiguration, Session $session, ModuleConfiguration $moduleConfiguration, - LoggerInterface $logger + LoggerInterface $logger, + HelpersManager $helpersManager ) { $this->sspConfiguration = $sspConfiguration; $this->session = $session; $this->moduleConfiguration = $moduleConfiguration; $this->logger = $logger; + $this->helpersManager = $helpersManager; } /** @@ -54,90 +58,18 @@ class Test * @return Template * @throws Exception */ - public function setup(Request $request): Template + public function test(Request $request): Template { $template = new Template($this->sspConfiguration, 'accounting:test.twig'); - $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger)) - ->build($this->moduleConfiguration->getJobsStoreClass()); + $retentionPolicy = new \DateInterval('P4D'); - // TODO refactor to tracker builder - $dataStoreConnectionKey = $this->moduleConfiguration->getClassConnectionKey(Tracker::class); - $dataStore = Store::build($this->moduleConfiguration, $this->logger, $dataStoreConnectionKey); + (new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger, $this->helpersManager)) + ->build($this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()) + ->enforceDataRetentionPolicy($retentionPolicy); - $jobsStoreNeedsSetup = $jobsStore->needsSetup(); - $jobsStoreSetupRan = false; + die('end'); - $dataStoreNeedsSetup = $dataStore->needsSetup(); - $dataStoreSetupRan = false; - - - if ($jobsStoreNeedsSetup && $request->query->has('setup')) { - $this->logger->info('Jobs Store setup ran.'); - $jobsStore->runSetup(); - $jobsStoreSetupRan = true; - } - - if ($dataStoreNeedsSetup && $request->query->has('setup')) { - $this->logger->info('Data Store setup ran.'); - $dataStore->runSetup(); - $dataStoreSetupRan = true; - } - - $sampleStateArray = ['Responder'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'sendResponse',],'\\SimpleSAML\\Auth\\State.exceptionFunc'=>[0=>'\\SimpleSAML\\Module\\saml\\IdP\\SAML2',1=>'handleAuthError',],'\\SimpleSAML\\Auth\\State.restartURL'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/SSOService.php?spentityid=https%3A%2F%2Fpc-example.org.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fsaml%2Fsp%2Fmetadata.php%2Fdefault-sp&RelayState=https%3A%2F%2Flocalhost.someone.from.hr%3A9074%2Fsimplesamlphp%2Fsimplesamlphp-2-beta-git%2Fmodule.php%2Fadmin%2Ftest%2Fdefault-sp&cookieTime=1660912195','SPMetadata'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'saml:RelayState'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/admin/test/default-sp','saml:RequestId'=>null,'saml:IDPList'=>[],'saml:ProxyCount'=>null,'saml:RequesterID'=>null,'ForceAuthn'=>false,'isPassive'=>false,'saml:ConsumerURL'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','saml:Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','saml:NameIDFormat'=>null,'saml:AllowCreate'=>true,'saml:Extensions'=>null,'saml:AuthnRequestReceivedAt'=>1660912195.505402,'saml:RequestedAuthnContext'=>null,'core:IdP'=>'saml2:https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','core:SP'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','IdPMetadata'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'ReturnCallback'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuth',],'Attributes'=>['hrEduPersonUniqueID'=>[0=>'testuser@primjer2.hr',],'urn:oid:0.9.2342.19200300.100.1.1'=>[0=>'testuser',],'urn:oid:2.5.4.3'=>[0=>'TestNameTestSurname',],'urn:oid:2.5.4.4'=>[0=>'TestSurname',1=>'TestSurname2',],'urn:oid:2.5.4.42'=>[0=>'TestName',],'urn:oid:0.9.2342.19200300.100.1.3'=>[0=>'testusermail@primjer.hr',1=>'testusermail2@primjer.hr',],'urn:oid:2.5.4.20'=>[0=>'123',],'hrEduPersonExtensionNumber'=>[0=>'123',],'urn:oid:0.9.2342.19200300.100.1.41'=>[0=>'123',],'urn:oid:2.5.4.23'=>[0=>'123',],'hrEduPersonUniqueNumber'=>[0=>'LOCAL_NO:100',1=>'OIB:00861590360',],'hrEduPersonOIB'=>[0=>'00861590360',],'hrEduPersonDateOfBirth'=>[0=>'20200101',],'hrEduPersonGender'=>[0=>'9',],'urn:oid:0.9.2342.19200300.100.1.60'=>[0=>'987654321',],'urn:oid:2.5.4.36'=>[0=>'cert-value',],'urn:oid:1.3.6.1.4.1.250.1.57'=>[0=>'http://www.org.hr/~ivekHomePage',],'hrEduPersonProfessionalStatus'=>[0=>'VSS',],'hrEduPersonAcademicStatus'=>[0=>'redovitiprofesor',],'hrEduPersonScienceArea'=>[0=>'sociologija',],'hrEduPersonAffiliation'=>[0=>'djelatnik',1=>'student',],'hrEduPersonPrimaryAffiliation'=>[0=>'djelatnik',],'hrEduPersonStudentCategory'=>[0=>'redovitistudent:diplomskisveučilišnistudij',],'hrEduPersonExpireDate'=>[0=>'20211231',],'hrEduPersonTitle'=>[0=>'rektor',],'hrEduPersonRole'=>[0=>'ICTkoordinator',1=>'ISVUkoordinator',],'hrEduPersonStaffCategory'=>[0=>'nastavnoosoblje',1=>'istraživači',],'hrEduPersonGroupMember'=>[0=>'grupa1',1=>'grupa2',],'urn:oid:2.5.4.10'=>[0=>'Testnaustanova',],'hrEduPersonHomeOrg'=>[0=>'primjer.hr',],'urn:oid:2.5.4.11'=>[0=>'Testnaorgjedinica',],'urn:oid:0.9.2342.19200300.100.1.6'=>[0=>'123',],'urn:oid:2.5.4.16'=>[0=>'Testnaustanova,Baštijanova52A,HR-10000Zagreb',],'urn:oid:2.5.4.7'=>[0=>'Zagreb',],'urn:oid:2.5.4.17'=>[0=>'HR-10000',],'urn:oid:2.5.4.9'=>[0=>'Baštijanova52A',],'urn:oid:0.9.2342.19200300.100.1.39'=>[0=>'Testnaadresa52A,HR-10000,Zagreb',],'urn:oid:0.9.2342.19200300.100.1.20'=>[0=>'123',],'hrEduPersonCommURI'=>[0=>'123',],'hrEduPersonPrivacy'=>[0=>'street',1=>'postalCode',],'hrEduPersonPersistentID'=>[0=>'da4294fb4e5746d57ab6ad88d2daf275',],'urn:oid:2.16.840.1.113730.3.1.241'=>[0=>'testname123',],'urn:oid:1.3.6.1.4.1.25178.1.2.12'=>[0=>'skype:pepe.perez',],'hrEduPersonCardNum'=>[0=>'123',],'updatedAt'=>[0=>'123456789',],'urn:oid:1.3.6.1.4.1.5923.1.1.1.7'=>[0=>'urn:example:oidc:manage:client',],],'Authority'=>'example-userpass','AuthnInstant'=>1660911943,'Expire'=>1660940743,'ReturnCall'=>[0=>'\\SimpleSAML\\IdP',1=>'postAuthProc',],'Destination'=>['SingleLogoutService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/singleLogoutService/default-sp',],],'AssertionConsumerService'=>[0=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>0,],1=>['Binding'=>'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Artifact','Location'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/assertionConsumerService/default-sp','index'=>1,],],'contacts'=>[0=>['emailAddress'=>'example@org.hr','givenName'=>'MarkoIvančić','contactType'=>'technical',],],'entityid'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-index'=>'https://pc-example.org.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/module.php/saml/sp/metadata.php/default-sp','metadata-set'=>'saml20-sp-remote',],'Source'=>['host'=>'localhost.someone.from.hr','privatekey'=>'key.pem','certificate'=>'cert.pem','auth'=>'example-userpass','attributes.NameFormat'=>'urn:oasis:names:tc:SAML:2.0:attrname-format:uri','authproc'=>[100=>['class'=>'core:AttributeMap',0=>'name2oid',],],'entityid'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-index'=>'https://localhost.someone.from.hr:9074/simplesamlphp/simplesamlphp-2-beta-git/saml2/idp/metadata.php','metadata-set'=>'saml20-idp-hosted',],'\\SimpleSAML\\Auth\\ProcessingChain.filters'=>[],]; - $state = new State($sampleStateArray); - - $authenticationEvent = new Event($state); - - //$dataStore->persist($authenticationEvent); - - $data = [ - 'jobs_store' => $this->moduleConfiguration->getJobsStoreClass(), - 'jobs_store_needs_setup' => $jobsStoreNeedsSetup ? 'yes' : 'no', - 'jobs_store_setup_ran' => $jobsStoreSetupRan ? 'yes' : 'no', - - 'data_store' => get_class($dataStore), - 'data_store_needs_setup' => $dataStoreNeedsSetup ? 'yes' : 'no', - 'data_store_setup_ran' => $dataStoreSetupRan ? 'yes' : 'no', - ]; - - die(var_dump($data)); - - $template->data = $data; return $template; } - - public function jobRunner():void - { - $jobsStore = (new JobsStoreBuilder($this->moduleConfiguration, $this->logger)) - ->build($this->moduleConfiguration->getJobsStoreClass()); - - - $trackerBuilder = new AuthenticationDataTrackerBuilder($this->moduleConfiguration, $this->logger); - - $configuredTrackers = array_merge( - [$this->moduleConfiguration->getDefaultDataTrackerAndProviderClass()], - $this->moduleConfiguration->getAdditionalTrackers() - ); - - $start = new \DateTimeImmutable(); - $data = [ - 'jobs_processed' => 0, - 'start_time' => $start->format('Y-m-d H:i:s.u'), - ]; - - /** @var Event\Job $job */ - while (($job = $jobsStore->dequeue(Event\Job::class)) !== null) { - foreach ($configuredTrackers as $tracker) { - ($trackerBuilder->build($tracker))->process($job->getPayload()); - $data['jobs_processed']++; - } - } - - $end = new \DateTimeImmutable(); - $data['end_time'] = $end->format('Y-m-d H:i:s.u'); - $data['duration'] = $end->diff($start)->format('%s seconds, %f microseconds'); - - die(var_dump($data)); - } } diff --git a/src/Http/Controllers/User/Profile.php b/src/Http/Controllers/User/Profile.php index 9f4d5ecf27a81218edb970f3330059b222a0d20c..52f392c33d632b26e319a53a5be2dd4e41ddf3cc 100644 --- a/src/Http/Controllers/User/Profile.php +++ b/src/Http/Controllers/User/Profile.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Http\Controllers\User; use Psr\Log\LoggerInterface; @@ -18,9 +20,6 @@ use SimpleSAML\XHTML\Template; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -/** - * @psalm-suppress all TODO mivanci remove this psalm suppress after testing - */ class Profile { protected ModuleConfiguration $moduleConfiguration; @@ -58,11 +57,11 @@ class Profile $this->defaultAuthenticationSource = $moduleConfiguration->getDefaultAuthenticationSource(); $this->authSimple = $authSimple ?? new Simple($this->defaultAuthenticationSource, $sspConfiguration, $session); - $this->authenticationDataProviderBuilder = $authenticationDataProviderBuilder ?? - new AuthenticationDataProviderBuilder($this->moduleConfiguration, $this->logger); - $this->helpersManager = $helpersManager ?? new HelpersManager(); + $this->authenticationDataProviderBuilder = $authenticationDataProviderBuilder ?? + new AuthenticationDataProviderBuilder($this->moduleConfiguration, $this->logger, $this->helpersManager); + // Make sure the end user is authenticated. $this->authSimple->requireAuth(); } @@ -80,7 +79,7 @@ class Profile foreach ($this->authSimple->getAttributes() as $name => $value) { // Convert attribute names to user-friendly names. if (array_key_exists($name, $toNameAttributeMap)) { - $name = $toNameAttributeMap[$name]; + $name = (string)$toNameAttributeMap[$name]; } $normalizedAttributes[$name] = implode('; ', $value); } @@ -114,7 +113,6 @@ class Profile $page = ($page = (int)$request->query->get('page', 1)) > 0 ? $page : 1; - // TODO mivanci make maxResults configurable $maxResults = 10; $firstResult = ($page - 1) * $maxResults; @@ -177,9 +175,9 @@ class Profile /** TODO mivanci remove after debugging */ protected function removeDebugDisplayLimits(): void { - ini_set('xdebug.var_display_max_depth', -1); - ini_set('xdebug.var_display_max_children', -1); - ini_set('xdebug.var_display_max_data', -1); + ini_set('xdebug.var_display_max_depth', '-1'); + ini_set('xdebug.var_display_max_children', '-1'); + ini_set('xdebug.var_display_max_data', '-1'); } protected function resolveTemplate(string $template): Template diff --git a/src/ModuleConfiguration.php b/src/ModuleConfiguration.php index 1827a1401191fc90bc710ae3e0262940b454a5ab..fe83f63151d45129b09dde07630cb4e0df2ca903 100644 --- a/src/ModuleConfiguration.php +++ b/src/ModuleConfiguration.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace SimpleSAML\Module\accounting; +use DateInterval; use Exception; use SimpleSAML\Configuration; use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; @@ -35,6 +36,8 @@ class ModuleConfiguration public const OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME = 'job_runner_maximum_execution_time'; public const OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED = 'job_runner_should_pause_after_number_of_jobs_processed'; + public const OPTION_TRACKER_DATA_RETENTION_POLICY = 'tracker_data_retention_policy'; + public const OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY = 'cron_tag_for_tracker_data_retention_policy'; /** * Contains configuration from module configuration file. @@ -80,7 +83,7 @@ class ModuleConfiguration return $this->getConfiguration()->getString(self::OPTION_JOBS_STORE); } - public function getJobRunnerMaximumExecutionTime(): ?\DateInterval + public function getJobRunnerMaximumExecutionTime(): ?DateInterval { $value = $this->get(self::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME); @@ -95,7 +98,7 @@ class ModuleConfiguration } try { - return new \DateInterval($value); + return new DateInterval($value); } catch (Throwable $exception) { $message = sprintf('Can not create DateInterval instance using value %s as parameter.', $value); throw new InvalidConfigurationException($message); @@ -139,6 +142,10 @@ class ModuleConfiguration return $this->getConfiguration()->getArray(self::OPTION_CONNECTIONS_AND_PARAMETERS); } + /** + * @return string[] + * @psalm-suppress MixedReturnTypeCoercion We specifically check if valid class string are provided. + */ public function getAdditionalTrackers(): array { return $this->getConfiguration()->getArray(self::OPTION_ADDITIONAL_TRACKERS); @@ -298,6 +305,13 @@ class ModuleConfiguration $errors[] = $exception->getMessage(); } + try { + $this->validateTrackerDataRetentionPolicy(); + } catch (Throwable $exception) { + $errors[] = $exception->getMessage(); + } + + if (!empty($errors)) { $message = sprintf('Module configuration validation failed with errors: %s', implode(' ', $errors)); throw new InvalidConfigurationException($message); @@ -343,9 +357,6 @@ class ModuleConfiguration $errors = []; // Validate additional trackers - /** - * @var string $trackerClass - */ foreach ($this->getAdditionalTrackers() as $trackerClass) { /** @psalm-suppress DocblockTypeContradiction */ if (!is_string($trackerClass)) { @@ -444,4 +455,34 @@ class ModuleConfiguration { $this->getCronTagForJobRunner(); } + + protected function validateTrackerDataRetentionPolicy(): void + { + if ($this->getTrackerDataRetentionPolicy() !== null) { + $this->getCronTagForTrackerDataRetentionPolicy(); + } + } + + public function getTrackerDataRetentionPolicy(): ?DateInterval + { + /** @var string|null $value */ + $value = $this->getConfiguration() + ->getOptionalString(self::OPTION_TRACKER_DATA_RETENTION_POLICY, null); + + if (is_null($value)) { + return null; + } + + try { + return new DateInterval($value); + } catch (Throwable $exception) { + $message = sprintf('Can not create DateInterval instance using value %s as parameter.', $value); + throw new InvalidConfigurationException($message); + } + } + + public function getCronTagForTrackerDataRetentionPolicy(): string + { + return $this->getConfiguration()->getString(self::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY); + } } diff --git a/src/Services/JobRunner.php b/src/Services/JobRunner.php index eb179489a113a185c12d7f411f0e188e2aa76ada..b08d96f17e4903a5b12b4def57108e3544f956b8 100644 --- a/src/Services/JobRunner.php +++ b/src/Services/JobRunner.php @@ -493,7 +493,6 @@ class JobRunner $this->moduleConfiguration->getAdditionalTrackers() ); - /** @var string $trackerClass */ foreach ($configuredTrackerClasses as $trackerClass) { $trackers[$trackerClass] = $this->authenticationDataTrackerBuilder->build($trackerClass); } diff --git a/src/Services/JobRunner/State.php b/src/Services/JobRunner/State.php index 74eb2e4afd714e5eb4e209213ded6fe9d9a4a8ac..3d1704927002a2464d3b06bc0f73b8da8f8c2246 100644 --- a/src/Services/JobRunner/State.php +++ b/src/Services/JobRunner/State.php @@ -15,6 +15,9 @@ class State protected ?\DateTimeImmutable $endedAt = null; protected int $successfulJobsProcessed = 0; protected int $failedJobsProcessed = 0; + /** + * @var string[] + */ protected array $statusMessages = []; protected int $numberOfStatusMessagesToKeep = 10; protected bool $isGracefulInterruptInitiated = false; @@ -161,6 +164,9 @@ class State } } + /** + * @return string[] + */ public function getStatusMessages(): array { return $this->statusMessages; @@ -172,7 +178,7 @@ class State return null; } - $message = (string)end($this->statusMessages); + $message = end($this->statusMessages); reset($this->statusMessages); return $message; diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php index 37319f1d486f33880628fd87d662883e4b9303e6..ce3478f1ed114d9a0734807e12326ddc285654c3 100644 --- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store.php @@ -506,76 +506,6 @@ class Store extends AbstractStore implements DataStoreInterface } return $connectedServiceProviderBag; - - // TODO mivanci remove after unit tests -// $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); -// $lastMetadataAndAttributesQueryBuilder = $this->connection->dbal()->createQueryBuilder(); -// -// /** @psalm-suppress TooManyArguments */ -// $authenticationEventsQueryBuilder->select( -// 'vs.entity_id AS sp_entity_id', -// 'COUNT(vae.id) AS number_of_authentications', -// 'MAX(vae.happened_at) AS last_authentication_at', -// 'MIN(vae.happened_at) AS first_authentication_at', -// )->from('vds_authentication_event', 'vae') -// ->leftJoin( -// 'vae', -// 'vds_idp_sp_user_version', -// 'visuv', -// 'vae.idp_sp_user_version_id = visuv.id' -// ) -// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') -// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') -// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') -// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') -// ->where( -// 'vu.identifier_hash_sha256 = ' . -// $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) -// ) -// ->groupBy('vs.id') -// ->orderBy('number_of_authentications', 'DESC'); -// -// /** @psalm-suppress TooManyArguments */ -// $lastMetadataAndAttributesQueryBuilder->select( -// 'vs.entity_id AS sp_entity_id', -// 'vsv.metadata AS sp_metadata', -// 'vuv.attributes AS user_attributes', -// // 'vsv.id AS sp_version_id', -// // 'vuv.id AS user_version_id', -// )->from('vds_authentication_event', 'vae') -// ->leftJoin( -// 'vae', -// 'vds_idp_sp_user_version', -// 'visuv', -// 'vae.idp_sp_user_version_id = visuv.id' -// ) -// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') -// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') -// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') -// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') -// ->leftJoin('vsv', 'vds_sp_version', 'vsv2', 'vsv.id = vsv2.id AND vsv.id < vsv2.id') -// ->leftJoin('vuv', 'vds_user_version', 'vuv2', 'vuv.id = vuv2.id AND vuv.id < vuv2.id') -// ->where( -// 'vu.identifier_hash_sha256 = ' . -// $lastMetadataAndAttributesQueryBuilder->createNamedParameter($userIdentifierHashSha256) -// ) -// ->andWhere('vsv2.id IS NULL') -// ->andWhere('vuv2.id IS NULL'); -// -// try { -// $numberOfAuthentications = -// $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); -// $lastMetadataAndAttributes = -// $lastMetadataAndAttributesQueryBuilder->executeQuery()->fetchAllAssociativeIndexed(); -// -// return array_merge_recursive($numberOfAuthentications, $lastMetadataAndAttributes); -// } catch (\Throwable $exception) { -// $message = sprintf( -// 'Error executing query to get connected organizations. Error was: %s.', -// $exception->getMessage() -// ); -// throw new StoreException($message, (int)$exception->getCode(), $exception); -// } } @@ -584,7 +514,6 @@ class Store extends AbstractStore implements DataStoreInterface */ public function getActivity(string $userIdentifierHashSha256, int $maxResults, int $firstResult): Activity\Bag { - // TODO mivanci pagination $results = $this->repository->getActivity($userIdentifierHashSha256, $maxResults, $firstResult); $activityBag = new Activity\Bag(); @@ -618,42 +547,11 @@ class Store extends AbstractStore implements DataStoreInterface } return $activityBag; + } - // TODO mivanci remove -// $authenticationEventsQueryBuilder = $this->connection->dbal()->createQueryBuilder(); -// -// /** @psalm-suppress TooManyArguments */ -// $authenticationEventsQueryBuilder->select( -// 'vae.happened_at', -// 'vsv.metadata AS sp_metadata', -// 'vuv.attributes AS user_attributes' -// )->from('vds_authentication_event', 'vae') -// ->leftJoin( -// 'vae', -// 'vds_idp_sp_user_version', -// 'visuv', -// 'vae.idp_sp_user_version_id = visuv.id' -// ) -// ->leftJoin('visuv', 'vds_sp_version', 'vsv', 'visuv.sp_version_id = vsv.id') -// ->leftJoin('vsv', 'vds_sp', 'vs', 'vsv.sp_id = vs.id') -// ->leftJoin('visuv', 'vds_user_version', 'vuv', 'visuv.user_version_id = vuv.id') -// ->leftJoin('vuv', 'vds_user', 'vu', 'vuv.user_id = vu.id') -// ->where( -// 'vu.identifier_hash_sha256 = ' . -// $authenticationEventsQueryBuilder->createNamedParameter($userIdentifierHashSha256) -// ) -// ->orderBy('vae.id', 'DESC'); -// -// try { -// $numberOfAuthentications = $authenticationEventsQueryBuilder->executeQuery()->fetchAllAssociative(); -// -// return array_merge_recursive($numberOfAuthentications); -// } catch (\Throwable $exception) { -// $message = sprintf( -// 'Error executing query to get connected organizations. Error was: %s.', -// $exception->getMessage() -// ); -// throw new StoreException($message, (int)$exception->getCode(), $exception); -// } + public function deleteDataOlderThan(\DateTimeImmutable $dateTime): void + { + // Only delete authentication events. Versioned data (IdP / SP metadata, user attributes) remain. + $this->repository->deleteAuthenticationEventsOlderThan($dateTime); } } diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php index 87938387f3de5f4a645b1b94e6d97928769802bd..be8e304cd55d8db5d95093d454758b2f3503ae3b 100644 --- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Migrations/Version20220801000700CreateAuthenticationEventTable.php @@ -50,6 +50,9 @@ class Version20220801000700CreateAuthenticationEventTable extends AbstractMigrat ['id'] ); + // Old data can be deleted using happened_at column, so add index for it. + $table->addIndex(['happened_at']); + $this->schemaManager->createTable($table); } catch (\Throwable $exception) { throw $this->prepareGenericMigrationException( diff --git a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php index 0c198896eea6dec297fd2b4270c15cc955bf2cb4..24c9b34492e51555959e33a3e482736b1ea7bd22 100644 --- a/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php +++ b/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/Repository.php @@ -1066,4 +1066,28 @@ class Repository throw new StoreException($message, (int)$exception->getCode(), $exception); } } + + /** + * @throws StoreException + */ + public function deleteAuthenticationEventsOlderThan(\DateTimeImmutable $dateTime): void + { + try { + $queryBuilder = $this->connection->dbal()->createQueryBuilder(); + + $queryBuilder->delete($this->tableNameAuthenticationEvent) + ->where( + $queryBuilder->expr()->lt( + TableConstants::TABLE_AUTHENTICATION_EVENT_COLUMN_NAME_HAPPENED_AT, + $queryBuilder->createNamedParameter($dateTime, Types::DATETIME_IMMUTABLE) + ) + )->executeStatement(); + } catch (Throwable $exception) { + $message = sprintf( + 'Error executing query to delete old authentication events. Error was: %s.', + $exception->getMessage() + ); + throw new StoreException($message, (int)$exception->getCode(), $exception); + } + } } diff --git a/src/Stores/Interfaces/DataStoreInterface.php b/src/Stores/Interfaces/DataStoreInterface.php index 7ba081678b8b5ba49d54345cc5baaf91de08f4f5..6fefe9a744454ade4ae0ffecdcb4dd045589ea01 100644 --- a/src/Stores/Interfaces/DataStoreInterface.php +++ b/src/Stores/Interfaces/DataStoreInterface.php @@ -24,4 +24,6 @@ interface DataStoreInterface extends StoreInterface public function getConnectedOrganizations(string $userIdentifierHashSha256): ConnectedServiceProvider\Bag; public function getActivity(string $userIdentifierHashSha256, int $maxResults, int $firstResult): Activity\Bag; + + public function deleteDataOlderThan(\DateTimeImmutable $dateTime): void; } diff --git a/src/Stores/Jobs/PhpRedis/RedisStore.php b/src/Stores/Jobs/PhpRedis/RedisStore.php index a2cfbfc60d024d84dfcb90a17aa6d38da92eabdb..20e6003b132f335fb3b78c26d606fe24c6372a44 100644 --- a/src/Stores/Jobs/PhpRedis/RedisStore.php +++ b/src/Stores/Jobs/PhpRedis/RedisStore.php @@ -137,6 +137,7 @@ class RedisStore extends AbstractStore implements JobsStoreInterface /** * @throws StoreException + * @codeCoverageIgnore */ public static function build( ModuleConfiguration $moduleConfiguration, diff --git a/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php index e974e9a2243f587339f2f09c586e04646443bdef..9c1971a0ebdd4fb1de97db1bc0631ab1904cff99 100644 --- a/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php +++ b/src/Trackers/Authentication/DoctrineDbal/Versioned/Tracker.php @@ -8,6 +8,7 @@ use Psr\Log\LoggerInterface; use SimpleSAML\Module\accounting\Entities\Activity; use SimpleSAML\Module\accounting\Entities\Authentication\Event; use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Providers\Interfaces\AuthenticationDataProviderInterface; use SimpleSAML\Module\accounting\Services\HelpersManager; @@ -84,4 +85,22 @@ class Tracker implements AuthenticationDataTrackerInterface, AuthenticationDataP $userIdentifierHashSha256 = $this->helpersManager->getHashHelper()->getSha256($userIdentifier); return $this->dataStore->getActivity($userIdentifierHashSha256, $maxResults, $firstResult); } + + public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void + { + $dateTime = (new \DateTimeImmutable())->sub($retentionPolicy); + + if ($dateTime === false) { + // @codeCoverageIgnoreStart + $message = sprintf( + 'Could not create DateTime instance for data retention policy enforcement. Retention policy was: %s.', + var_export($retentionPolicy, true) + ); + $this->logger->error($message); + throw new InvalidConfigurationException($message); + // @codeCoverageIgnoreEnd + } + + $this->dataStore->deleteDataOlderThan($dateTime); + } } diff --git a/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php index b3b95720279ab21603920b762477553244593833..9a1f826bd2420a0701b2289de541c2c89dddfd73 100644 --- a/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php +++ b/src/Trackers/Interfaces/AuthenticationDataTrackerInterface.php @@ -1,5 +1,7 @@ <?php +declare(strict_types=1); + namespace SimpleSAML\Module\accounting\Trackers\Interfaces; use Psr\Log\LoggerInterface; @@ -14,4 +16,6 @@ interface AuthenticationDataTrackerInterface extends BuildableUsingModuleConfigu public static function build(ModuleConfiguration $moduleConfiguration, LoggerInterface $logger): self; public function process(Event $authenticationEvent): void; + + public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void; } diff --git a/templates/admin/configuration/status.twig b/templates/admin/configuration/status.twig index 43ea6c53d089f104310d703b96d55d3e35beeee6..9927f73ac568730751181d03c3fd9075b326c5b5 100644 --- a/templates/admin/configuration/status.twig +++ b/templates/admin/configuration/status.twig @@ -28,10 +28,20 @@ <strong>{{ 'Tracker and Provider Setup Needed'|trans }}</strong>: {{ defaultDataTrackerAndProvider.needsSetup ? 'Yes'|trans : 'No'|trans }} </li> + {% if additionalTrackers is not empty %} + <li> + <strong>{{ 'Additional Trackers and setup'|trans }}</strong>: + <ul> + {% for trackerClass, trackerInstance in additionalTrackers %} + <li> + {{ trackerClass }}: {{ trackerInstance.needsSetup ? 'Yes'|trans : 'No'|trans }} + </li> + {% endfor %} + </ul> + </li> + {% endif %} </ul> -{# TODO mivanci setup for additional trackers#} - {% if moduleConfiguration.getAccountingProcessingType == 'asynchronous' %} <ul> <li> diff --git a/tests/config-templates/module_accounting.php b/tests/config-templates/module_accounting.php index 82daf2b42edd594c4368cf2efeb6c4019d02ff5d..882170befdeb1d19e4908df67adb8402c21eee02 100644 --- a/tests/config-templates/module_accounting.php +++ b/tests/config-templates/module_accounting.php @@ -16,8 +16,6 @@ $config = [ ModuleConfiguration::OPTION_ACCOUNTING_PROCESSING_TYPE => ModuleConfiguration\AccountingProcessingType::VALUE_SYNCHRONOUS, - ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner', - ModuleConfiguration::OPTION_JOBS_STORE => Stores\Jobs\DoctrineDbal\Store::class, ModuleConfiguration::OPTION_DEFAULT_DATA_TRACKER_AND_PROVIDER => @@ -53,4 +51,11 @@ $config = [ ModuleConfiguration::OPTION_JOB_RUNNER_MAXIMUM_EXECUTION_TIME => null, ModuleConfiguration::OPTION_JOB_RUNNER_SHOULD_PAUSE_AFTER_NUMBER_OF_JOBS_PROCESSED => 10, + + ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => null, + + ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY => + 'accounting_tracker_data_retention_policy', + + ModuleConfiguration::OPTION_CRON_TAG_FOR_JOB_RUNNER => 'accounting_job_runner', ]; diff --git a/tests/src/ModuleConfigurationTest.php b/tests/src/ModuleConfigurationTest.php index db2a3d286e794583904a4fa4272b0649f3432ef9..50bc22290cc94da2e0769762d596448b3f460254 100644 --- a/tests/src/ModuleConfigurationTest.php +++ b/tests/src/ModuleConfigurationTest.php @@ -324,4 +324,29 @@ class ModuleConfigurationTest extends TestCase ] ); } + + public function testThrowsForInvalidTrackerDataRetentionPolicy(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration( + null, + [ + ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'invalid' + ] + ); + } + + public function testThrowsForInvalidCronTagForTrackerDataRetentionPolicy(): void + { + $this->expectException(InvalidConfigurationException::class); + + new ModuleConfiguration( + null, + [ + ModuleConfiguration::OPTION_TRACKER_DATA_RETENTION_POLICY => 'P1D', + ModuleConfiguration::OPTION_CRON_TAG_FOR_TRACKER_DATA_RETENTION_POLICY => false, + ] + ); + } } diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php index ef1b0e1b96e6f6152dc7aa64b2f9a3b11baa92ac..63144f4399de5686a7c2eb91673614d5cb7e6018 100644 --- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/Store/RepositoryTest.php @@ -727,4 +727,63 @@ class RepositoryTest extends TestCase $repository->getActivity($this->userIdentifierHash, 10, 0); } + + public function testCanDeleteAuthenticationEventsOlderThan(): void + { + $this->repository->insertIdp($this->idpEntityId, $this->idpEntityIdHash, $this->createdAt); + $idpResult = $this->repository->getIdp($this->idpEntityIdHash)->fetchAssociative(); + $idpId = (int)$idpResult[Store\TableConstants::TABLE_IDP_COLUMN_NAME_ID]; + $this->repository->insertIdpVersion($idpId, $this->idpMetadata, $this->idpMetadataHash, $this->createdAt); + $idpVersionResult = $this->repository->getIdpVersion($idpId, $this->idpMetadataHash)->fetchAssociative(); + + $this->repository->insertSp($this->spEntityId, $this->spEntityIdHash, $this->createdAt); + $spResult = $this->repository->getSp($this->spEntityIdHash)->fetchAssociative(); + $spId = (int)$spResult[Store\TableConstants::TABLE_SP_COLUMN_NAME_ID]; + $this->repository->insertSpVersion($spId, $this->spMetadata, $this->spMetadataHash, $this->createdAt); + $spVersionResult = $this->repository->getSpVersion($spId, $this->spMetadataHash)->fetchAssociative(); + + $this->repository->insertUser($this->userIdentifier, $this->userIdentifierHash, $this->createdAt); + $userResult = $this->repository->getUser($this->userIdentifierHash)->fetchAssociative(); + $userId = (int)$userResult[Store\TableConstants::TABLE_USER_COLUMN_NAME_ID]; + $this->repository + ->insertUserVersion($userId, $this->userAttributes, $this->userAttributesHash, $this->createdAt); + $userVersionResult = $this->repository->getUserVersion($userId, $this->userAttributesHash)->fetchAssociative(); + + $idpVersionId = (int)$idpVersionResult[Store\TableConstants::TABLE_IDP_VERSION_COLUMN_NAME_ID]; + $spVersionId = (int)$spVersionResult[Store\TableConstants::TABLE_SP_VERSION_COLUMN_NAME_ID]; + $userVersionId = (int)$userVersionResult[Store\TableConstants::TABLE_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId, $this->createdAt); + $idpSpUserVersionResult = $this->repository->getIdpSpUserVersion($idpVersionId, $spVersionId, $userVersionId) + ->fetchAssociative(); + + $idpSpUserVersionId = + (int)$idpSpUserVersionResult[Store\TableConstants::TABLE_IDP_SP_USER_VERSION_COLUMN_NAME_ID]; + + $this->repository->insertAuthenticationEvent( + $idpSpUserVersionId, + $this->createdAt, + $this->clientIpAddress, + $this->createdAt + ); + + $resultArray = $this->repository->getActivity($this->userIdentifierHash, 10, 0); + $this->assertCount(1, $resultArray); + + $dateTimeInFuture = $this->createdAt->add(new \DateInterval('P1D')); + + $this->repository->deleteAuthenticationEventsOlderThan($dateTimeInFuture); + + $resultArray = $this->repository->getActivity($this->userIdentifierHash, 10, 0); + $this->assertCount(0, $resultArray); + } + + public function testDeleteAuthenticationEventsOlderThanThrowsOnInvalidDbal(): void + { + $this->connectionStub->method('dbal')->willThrowException(new \Exception('test')); + $repository = new Repository($this->connectionStub, $this->loggerStub); + $this->expectException(StoreException::class); + + $repository->deleteAuthenticationEventsOlderThan(new \DateTimeImmutable()); + } } diff --git a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php index e1aa07db55a6f0a8c6ff01f17b3d04ef2aeb035e..8910b95cb28b90cb2d2387dd057e7d13d929e110 100644 --- a/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php +++ b/tests/src/Stores/Data/Authentication/DoctrineDbal/Versioned/StoreTest.php @@ -69,15 +69,15 @@ class StoreTest extends TestCase protected Event $authenticationEvent; protected Store\HashDecoratedState $hashDecoratedState; /** - * @var \PHPUnit\Framework\MockObject\Stub|Store\Repository|Store\Repository&\PHPUnit\Framework\MockObject\Stub + * @var \PHPUnit\Framework\MockObject\MockObject|Store\Repository */ - protected $repositoryStub; + protected $repositoryMock; /** - * @var Result|Result&\PHPUnit\Framework\MockObject\Stub|\PHPUnit\Framework\MockObject\Stub + * @var Result|\PHPUnit\Framework\MockObject\Stub */ protected $resultStub; /** - * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface|LoggerInterface&\PHPUnit\Framework\MockObject\MockObject + * @var \PHPUnit\Framework\MockObject\MockObject|LoggerInterface */ protected $loggerMock; @@ -105,7 +105,7 @@ class StoreTest extends TestCase $this->authenticationEvent = new Event($this->state); $this->hashDecoratedState = new Store\HashDecoratedState($this->state); - $this->repositoryStub = $this->createStub(Store\Repository::class); + $this->repositoryMock = $this->createMock(Store\Repository::class); $this->resultStub = $this->createStub(Result::class); } @@ -240,7 +240,7 @@ class StoreTest extends TestCase public function testResolveIdpIdThrowsOnFirstGetIdpFailure(): void { - $this->repositoryStub->method('getIdp')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdp')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -248,7 +248,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -259,8 +259,8 @@ class StoreTest extends TestCase public function testResolveIdpIdThrowsOnInsertAndGetIdpFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getIdp')->willReturn($this->resultStub); - $this->repositoryStub->method('insertIdp')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdp')->willReturn($this->resultStub); + $this->repositoryMock->method('insertIdp')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -269,7 +269,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -280,7 +280,7 @@ class StoreTest extends TestCase public function testResolveIdpVersionIdThrowsOnFirstGetIdpVersionFailure(): void { - $this->repositoryStub->method('getIdpVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdpVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -288,7 +288,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -299,8 +299,8 @@ class StoreTest extends TestCase public function testResolveIdpVersionIdThrowsOnInsertAndGetIdpVersionFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getIdpVersion')->willReturn($this->resultStub); - $this->repositoryStub->method('insertIdpVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdpVersion')->willReturn($this->resultStub); + $this->repositoryMock->method('insertIdpVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -309,7 +309,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -320,7 +320,7 @@ class StoreTest extends TestCase public function testResolveSpIdThrowsOnFirstGetSpFailure(): void { - $this->repositoryStub->method('getSp')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getSp')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -328,7 +328,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -339,8 +339,8 @@ class StoreTest extends TestCase public function testResolveSpIdThrowsOnInsertAndGetSpFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getSp')->willReturn($this->resultStub); - $this->repositoryStub->method('insertSp')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getSp')->willReturn($this->resultStub); + $this->repositoryMock->method('insertSp')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -349,7 +349,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -360,7 +360,7 @@ class StoreTest extends TestCase public function testResolveSpVersionIdThrowsOnFirstGetSpVersionFailure(): void { - $this->repositoryStub->method('getSpVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getSpVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -368,7 +368,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -379,8 +379,8 @@ class StoreTest extends TestCase public function testResolveSpVersionIdThrowsOnInsertAndGetSpVersionFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getSpVersion')->willReturn($this->resultStub); - $this->repositoryStub->method('insertSpVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getSpVersion')->willReturn($this->resultStub); + $this->repositoryMock->method('insertSpVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -389,7 +389,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -410,7 +410,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(UnexpectedValueException::class); @@ -420,7 +420,7 @@ class StoreTest extends TestCase public function testResolveUserIdThrowsOnFirstGetUserFailure(): void { - $this->repositoryStub->method('getUser')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getUser')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -428,7 +428,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -439,8 +439,8 @@ class StoreTest extends TestCase public function testResolveUserIdThrowsOnInsertAndGetUserFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getUser')->willReturn($this->resultStub); - $this->repositoryStub->method('insertUser')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getUser')->willReturn($this->resultStub); + $this->repositoryMock->method('insertUser')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -449,7 +449,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -460,7 +460,7 @@ class StoreTest extends TestCase public function testResolveUserVersionIdThrowsOnFirstGetUserVersionFailure(): void { - $this->repositoryStub->method('getUserVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getUserVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -468,7 +468,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -479,8 +479,8 @@ class StoreTest extends TestCase public function testResolveUserVersionIdThrowsOnInsertAndGetUserVersionFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getUserVersion')->willReturn($this->resultStub); - $this->repositoryStub->method('insertUserVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getUserVersion')->willReturn($this->resultStub); + $this->repositoryMock->method('insertUserVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -489,7 +489,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -500,7 +500,7 @@ class StoreTest extends TestCase public function testResolveIdpSpUserVersionIdThrowsOnFirstGetIdpSpUserVersionFailure(): void { - $this->repositoryStub->method('getIdpSpUserVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdpSpUserVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( $this->moduleConfigurationStub, @@ -508,7 +508,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -519,8 +519,8 @@ class StoreTest extends TestCase public function testResolveIdpSpUserVersionIdThrowsOnInsertAndGetIdpSpUserVersionFailure(): void { $this->resultStub->method('fetchOne')->willReturn(false); - $this->repositoryStub->method('getIdpSpUserVersion')->willReturn($this->resultStub); - $this->repositoryStub->method('insertIdpSpUserVersion')->willThrowException(new \Exception('test')); + $this->repositoryMock->method('getIdpSpUserVersion')->willReturn($this->resultStub); + $this->repositoryMock->method('insertIdpSpUserVersion')->willThrowException(new \Exception('test')); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -529,7 +529,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -540,7 +540,7 @@ class StoreTest extends TestCase public function testGetConnectedOrganizationsReturnsEmptyBagIfNoResults(): void { - $this->repositoryStub->method('getConnectedServiceProviders')->willReturn([]); + $this->repositoryMock->method('getConnectedServiceProviders')->willReturn([]); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -549,7 +549,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $connectedServiceProviderBag = $store->getConnectedOrganizations('test'); @@ -559,7 +559,7 @@ class StoreTest extends TestCase public function testCanGetConnectedOrganizationsBag(): void { - $this->repositoryStub->method('getConnectedServiceProviders') + $this->repositoryMock->method('getConnectedServiceProviders') ->willReturn([RawRowResult::CONNECTED_ORGANIZATION]); /** @psalm-suppress InvalidArgument */ @@ -569,7 +569,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $connectedServiceProviderBag = $store->getConnectedOrganizations('test'); @@ -582,7 +582,7 @@ class StoreTest extends TestCase $rawResult = RawRowResult::CONNECTED_ORGANIZATION; unset($rawResult[TableConstants::ENTITY_CONNECTED_ORGANIZATION_COLUMN_NAME_NUMBER_OF_AUTHENTICATIONS]); - $this->repositoryStub->method('getConnectedServiceProviders') + $this->repositoryMock->method('getConnectedServiceProviders') ->willReturn([$rawResult]); /** @psalm-suppress InvalidArgument */ @@ -592,7 +592,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); @@ -601,7 +601,7 @@ class StoreTest extends TestCase public function testGetActivityReturnsEmptyBagIfNoResults(): void { - $this->repositoryStub->method('getActivity')->willReturn([]); + $this->repositoryMock->method('getActivity')->willReturn([]); /** @psalm-suppress InvalidArgument */ $store = new Store( @@ -610,7 +610,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $activityBag = $store->getActivity('test', 10, 0); @@ -620,7 +620,7 @@ class StoreTest extends TestCase public function testCanGetActivityBag(): void { - $this->repositoryStub->method('getActivity') + $this->repositoryMock->method('getActivity') ->willReturn([RawRowResult::ACTIVITY]); /** @psalm-suppress InvalidArgument */ @@ -630,7 +630,7 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $activityBag = $store->getActivity('test', 10, 0); @@ -643,7 +643,7 @@ class StoreTest extends TestCase $rawResult = RawRowResult::ACTIVITY; unset($rawResult[TableConstants::ENTITY_ACTIVITY_COLUMN_NAME_HAPPENED_AT]); - $this->repositoryStub->method('getActivity') + $this->repositoryMock->method('getActivity') ->willReturn([$rawResult]); /** @psalm-suppress InvalidArgument */ @@ -653,10 +653,31 @@ class StoreTest extends TestCase null, ModuleConfiguration\ConnectionType::MASTER, $this->factoryStub, - $this->repositoryStub + $this->repositoryMock ); $this->expectException(StoreException::class); $store->getActivity('test', 10, 0); } + + public function testCanDeleteDataOlderThan(): void + { + $dateTime = new \DateTimeImmutable(); + + $this->repositoryMock->expects($this->once()) + ->method('deleteAuthenticationEventsOlderThan') + ->with($dateTime); + + /** @psalm-suppress InvalidArgument */ + $store = new Store( + $this->moduleConfigurationStub, + $this->loggerMock, + null, + ModuleConfiguration\ConnectionType::MASTER, + $this->factoryStub, + $this->repositoryMock + ); + + $store->deleteDataOlderThan($dateTime); + } } diff --git a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php index 58ed6379dca8d31672ac57a1692bc9d98351ad1d..a6800b562b5af0da2cc92d900c5c5438929284fc 100644 --- a/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php +++ b/tests/src/Trackers/Authentication/DoctrineDbal/Versioned/TrackerTest.php @@ -8,6 +8,7 @@ use Psr\Log\LoggerInterface; use SimpleSAML\Module\accounting\Entities\Activity; use SimpleSAML\Module\accounting\Entities\Authentication\Event; use SimpleSAML\Module\accounting\Entities\ConnectedServiceProvider; +use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException; use SimpleSAML\Module\accounting\ModuleConfiguration; use SimpleSAML\Module\accounting\Services\HelpersManager; use SimpleSAML\Module\accounting\Stores\Data\Authentication\DoctrineDbal\Versioned\Store; @@ -194,4 +195,22 @@ class TrackerTest extends TestCase $tracker->getActivity('test', 10, 0) ); } + + public function testCanEnforceDataRetentionPolicy(): void + { + $retentionPolicy = new \DateInterval('P10D'); + + $this->dataStoreMock->expects($this->once()) + ->method('deleteDataOlderThan'); + + $tracker = new Tracker( + $this->moduleConfigurationStub, + $this->loggerMock, + ModuleConfiguration\ConnectionType::MASTER, + $this->helpersManagerStub, + $this->dataStoreMock + ); + + $tracker->enforceDataRetentionPolicy($retentionPolicy); + } } diff --git a/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php index cf3bae0c93acf0b4ca09469393c815bcbbaaab51..8a27f1a4ab3c306b1e80521439bd976068449873 100644 --- a/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php +++ b/tests/src/Trackers/Builders/AuthenticationDataTrackerBuilderTest.php @@ -64,6 +64,10 @@ class AuthenticationDataTrackerBuilderTest extends TestCase public function runSetup(): void { } + + public function enforceDataRetentionPolicy(\DateInterval $retentionPolicy): void + { + } }; }