Unverified Commit 585dee1a authored by Marko Ivancic's avatar Marko Ivancic Committed by GitHub
Browse files

Cleanup (#14)



Co-authored-by: Marko Ivancic's avatarMarko Ivančić <marko.ivancic@srce.hr>
parent 91e8b8fa
Pipeline #73935 passed with stage
in 1 minute and 54 seconds
name: Test
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test-74:
runs-on: ubuntu-latest
container:
image: cicnavi/dap:74
steps:
- uses: actions/checkout@v3
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-${{ job.container.id }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ job.container.id }}-php-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run test suite
run: composer run-script pre-commit
- name: Show PHP version
run: php -v
test-latest:
runs-on: ubuntu-latest
container:
image: cicnavi/dap:08
steps:
- uses: actions/checkout@v3
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache Composer packages
id: composer-cache
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-${{ job.container.id }}-php-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-${{ job.container.id }}-php-
- name: Install dependencies
if: steps.composer-cache.outputs.cache-hit != 'true'
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run test suite
run: composer run-script pre-commit
- name: Show PHP version
run: php -v
\ No newline at end of file
# Composer stores all downloaded packages in the vendor/ directory.
# Do not use the following if the vendor/ directory is committed to
# your git repository.
cache:
# We key the cache using the commit unique identifier.
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
# List of stages for jobs, and their order of execution
stages:
- test
# PHP v7.4
test-74:
stage: test
image: cicnavi/dap:74
script:
- composer install --prefer-dist --no-progress --no-suggest
- composer run-script pre-commit
# PHP latest
test-08:
stage: test
image: cicnavi/dap:08
script:
- composer install --prefer-dist --no-progress --no-suggest
- composer run-script pre-commit
[![Test](https://github.com/cicnavi/simplesamlphp-module-accounting/actions/workflows/test.yml/badge.svg)](https://github.com/cicnavi/simplesamlphp-module-accounting/actions/workflows/test.yml)
# simplesamlphp-module-accounting
SimpleSAMLphp module providing user accounting functionality using SimpleSAMLphp authentication processing
filters feature.
......@@ -32,6 +34,17 @@ Module is installable using Composer:
composer require cicnavi/simplesamlphp-module-accounting
```
In config.php, search for the "module.enable" key and set 'accounting' to true:
```php
// ...
'module.enable' => [
'accounting' => true,
// ...
],
// ...
```
Depending on used features, module also requires:
- ext-redis: if PhpRedis is to be used as a store
......@@ -112,6 +125,19 @@ in config/config.php:
],
// ...
```
## Job Runner
If accounting processing is asynchronous, a job runner will have to be used in order to process jobs that have
been created during authentication events.
Job runner can be executed using [SimpleSAMLphp Cron module](https://github.com/simplesamlphp/simplesamlphp/blob/master/modules/cron/docs/cron.md).
As you can see in Cron documentation, a cron tag can be invoked using HTTP or CLI. When it comes to Job Runner, using
CLI is the preferred way, since the job runner can run in a long-running fashion, even indefinitely. However,
you are free to test execution using the HTTP version, in which case the maximum job runner execution time
will correspond to the 'max_execution_time' INI setting.
Only one job runner instance can run at given point in time. By maintaining internal state, job runner can first check
if there is another job runner active. If yes, the latter will simply exit and let the active job runner do its work.
This way one is free to invoke the cron tag at any time, since only one job runner will ever be active.
## TODO
- [ ] Translation
......
#!/usr/bin/env php
<?php
// TODO mivanci remove this file before release
declare(strict_types=1);
use SimpleSAML\Module\accounting\Entities\Authentication\Event;
use SimpleSAML\Module\accounting\Entities\Authentication\Event\Job;
use SimpleSAML\Module\accounting\Entities\Authentication\State;
use SimpleSAML\Module\accounting\Services\HelpersManager;
use SimpleSAML\Module\accounting\Services\Logger;
use SimpleSAML\Module\accounting\Stores\Connections\DoctrineDbal\Connection;
use SimpleSAML\Module\accounting\Stores\Jobs\DoctrineDbal\Store\Repository;
use SimpleSAML\Module\accounting\Stores\Jobs\PhpRedis\RedisStore;
require 'vendor/autoload.php';
$helpersManager = new HelpersManager();
$start = new DateTime();
$newLine = "\n";
echo "Start: " . $start->format(DateTime::ATOM);
echo $newLine;
$job = new Job(new Event(new State(SimpleSAML\Test\Module\accounting\Constants\StateArrays::FULL)));
$options = getopt('c:');
$numberOfItems = $options['c'] ?? 1000;
echo 'Number of items: ' . $numberOfItems;
echo $newLine;
$spinnerChars = ['|', '/', '-', '\\'];
/**/
echo 'Starting simulating MySQL: ';
$mysqlStartTime = new DateTime();
echo $mysqlStartTime->format(DateTime::ATOM);
echo $newLine;
$mysqlParameters = [
'driver' => 'pdo_mysql', // (string): The built-in driver implementation to use
'user' => 'apps', // (string): Username to use when connecting to the database.
'password' => 'apps', // (string): Password to use when connecting to the database.
'host' => '127.0.0.1', // (string): Hostname of the database to connect to.
'port' => 33306, // (integer): Port of the database to connect to.
'dbname' => 'accounting', // (string): Name of the database/schema to connect to.
//'unix_socket' => 'unix_socet', // (string): Name of the socket used to connect to the database.
'charset' => 'utf8', // (string): The charset used when connecting to the database.
//'url' => 'mysql://user:secret@localhost/mydb?charset=utf8', // ...alternative way of providing parameters.
// Additional parameters not originally avaliable in Doctrine DBAL
'table_prefix' => '', // (string): Prefix for each table.
];
$logger = new Logger();
$jobsStoreRepository = new Repository(new Connection($mysqlParameters), 'job', $logger);
$mysqlDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp();
$mysqlItemsInCurrentSecond = 0;
$mysqlItemsPerSecond = [];
for ($i = 1; $i <= $numberOfItems; $i++) {
$mysqlUpdatedDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp();
if ($mysqlDurationInSeconds === $mysqlUpdatedDurationInSeconds) {
$mysqlItemsInCurrentSecond++;
} else {
$mysqlItemsPerSecond[] = $mysqlItemsInCurrentSecond;
$mysqlItemsInCurrentSecond = 0;
}
$mysqlItemsInCurrentSecond = $mysqlDurationInSeconds === $mysqlUpdatedDurationInSeconds ?
$mysqlItemsInCurrentSecond++ : 0;
$mysqlDurationInSeconds = (new DateTime())->getTimestamp() - $mysqlStartTime->getTimestamp();
$mysqlItemsPerSeconds = count($mysqlItemsPerSecond) ?
array_sum($mysqlItemsPerSecond) / count($mysqlItemsPerSecond) : 0;
$mysqlPercentage = $i / $numberOfItems * 100;
$spinnerChar = $spinnerChars[array_rand($spinnerChars)];
$line = sprintf(
'%1$s percentage: %2$ 3d%%, items/s: %3$04d, duration: %4$ss',
$spinnerChar, $mysqlPercentage, $mysqlItemsPerSeconds, $mysqlDurationInSeconds
);
echo $line;
echo "\r";
$jobsStoreRepository->insert($job);
}
echo $newLine;
echo $newLine;
echo 'Starting simulating Redis: ';
$redisStartTime = new DateTime();
echo $redisStartTime->format(DateTime::ATOM);
echo $newLine;
$redisClient = new Redis();
$redisClient->connect(
'127.0.0.1',
6379,
1,
null,
500,
1
);
$redisClient->auth('apps');
$redisClient->setOption(Redis::OPT_PREFIX, 'ssp_accounting:');
$redisDurationInSeconds = (new DateTime())->getTimestamp() - $redisStartTime->getTimestamp();
$redisItemsInCurrentSecond = 0;
$redisItemsPerSecond = [];
for ($i = 1; $i <= $numberOfItems; $i++) {
$redisUpdatedDurationInSeconds = (new DateTime())->getTimestamp() - $redisStartTime->getTimestamp();
if ($redisDurationInSeconds === $redisUpdatedDurationInSeconds) {
$redisItemsInCurrentSecond++;
} else {
$redisItemsPerSecond[] = $redisItemsInCurrentSecond;
$redisItemsInCurrentSecond = 0;
}
$redisItemsInCurrentSecond = $redisDurationInSeconds === $redisUpdatedDurationInSeconds ?
$redisItemsInCurrentSecond++ : 0;
$redisDurationInSeconds = $redisUpdatedDurationInSeconds;
$redisItemsPerSeconds = count($redisItemsPerSecond) ?
array_sum($redisItemsPerSecond) / count($redisItemsPerSecond) : 0;
$redisPercentage = $i / $numberOfItems * 100;
$spinnerChar = $spinnerChars[array_rand($spinnerChars)];
$line = sprintf(
'%1$s percentage: %2$ 3d%%, items/s: %3$04d, duration: %4$ss',
$spinnerChar, $redisPercentage, $redisItemsPerSeconds, $redisDurationInSeconds
);
echo $line;
echo "\r";
$redisClient->rPush(RedisStore::LIST_KEY_JOB . ':' . sha1($job->getType()), serialize($job));
// $redisClient->rPush(RedisStore::LIST_KEY_JOB, serializgit add .e($job));
}
echo $newLine;
echo 'End: ' . (new DateTime())->format(DateTime::ATOM);
echo $newLine;
\ No newline at end of file
......@@ -5,8 +5,10 @@ declare(strict_types=1);
use SimpleSAML\Locale\Translate;
use SimpleSAML\Module\accounting\Helpers\ModuleRoutesHelper;
use SimpleSAML\Module\accounting\ModuleConfiguration;
use SimpleSAML\XHTML\Template;
function accounting_hook_adminmenu(\SimpleSAML\XHTML\Template &$template): void
/** @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection Reference is actually used by SimpleSAMLphp */
function accounting_hook_adminmenu(Template &$template): void
{
$menuKey = 'menu';
......
......@@ -7,6 +7,7 @@ use SimpleSAML\Module\accounting\Helpers\ModuleRoutesHelper;
use SimpleSAML\Module\accounting\ModuleConfiguration;
use SimpleSAML\XHTML\Template;
/** @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection Reference is used by SimpleSAMLphp */
function accounting_hook_configpage(Template &$template): void
{
$moduleRoutesHelper = new ModuleRoutesHelper();
......
......@@ -70,6 +70,9 @@ function accounting_hook_cron(array &$cronInfo): void
}
}
/**
* @throws \SimpleSAML\Module\accounting\Exceptions\Exception
*/
function handleDataRetentionPolicy(
ModuleConfiguration $moduleConfiguration,
LoggerInterface $logger,
......
# 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
defaults: { _controller: 'SimpleSAML\Module\accounting\Http\Controllers\Admin\Configuration::status' }
......
......@@ -14,6 +14,7 @@ use SimpleSAML\Module\accounting\Services\HelpersManager;
use SimpleSAML\Module\accounting\Services\Logger;
use SimpleSAML\Module\accounting\Stores\Builders\JobsStoreBuilder;
use SimpleSAML\Module\accounting\Trackers\Builders\AuthenticationDataTrackerBuilder;
use Throwable;
class Accounting extends ProcessingFilter
{
......@@ -54,6 +55,7 @@ class Accounting extends ProcessingFilter
}
/**
* @noinspection PhpParameterByRefIsNotUsedAsReferenceInspection Reference is actually used by SimpleSAMLphp
*/
public function process(array &$state): void
{
......@@ -75,7 +77,7 @@ class Accounting extends ProcessingFilter
foreach ($configuredTrackers as $tracker) {
($this->authenticationDataTrackerBuilder->build($tracker))->process($authenticationEvent);
}
} catch (\Throwable $exception) {
} catch (Throwable $exception) {
$message = sprintf('Accounting error, skipping... Error was: %s.', $exception->getMessage());
$this->logger->error($message, $state);
}
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Entities\Activity;
use SimpleSAML\Module\accounting\Entities\Activity;
......
......@@ -4,17 +4,18 @@ declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Entities\Authentication;
use DateTimeImmutable;
use SimpleSAML\Module\accounting\Entities\Bases\AbstractPayload;
class Event extends AbstractPayload
{
protected State $state;
protected \DateTimeImmutable $happenedAt;
protected DateTimeImmutable $happenedAt;
public function __construct(State $state, \DateTimeImmutable $happenedAt = null)
public function __construct(State $state, DateTimeImmutable $happenedAt = null)
{
$this->state = $state;
$this->happenedAt = $happenedAt ?? new \DateTimeImmutable();
$this->happenedAt = $happenedAt ?? new DateTimeImmutable();
}
public function getState(): State
......@@ -22,7 +23,7 @@ class Event extends AbstractPayload
return $this->state;
}
public function getHappenedAt(): \DateTimeImmutable
public function getHappenedAt(): DateTimeImmutable
{
return $this->happenedAt;
}
......
......@@ -4,10 +4,11 @@ declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Entities\Authentication;
use DateTimeImmutable;
use SimpleSAML\Module\accounting\Entities\Bases\AbstractProvider;
use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
use SimpleSAML\Module\accounting\Helpers\NetworkHelper;
use SimpleSAML\Module\accounting\Services\HelpersManager;
use Throwable;
class State
{
......@@ -24,8 +25,8 @@ class State
protected string $identityProviderEntityId;
protected string $serviceProviderEntityId;
protected array $attributes;
protected \DateTimeImmutable $createdAt;
protected ?\DateTimeImmutable $authenticationInstant;
protected DateTimeImmutable $createdAt;
protected ?DateTimeImmutable $authenticationInstant;
protected array $identityProviderMetadata;
protected array $serviceProviderMetadata;
protected ?string $clientIpAddress;
......@@ -33,10 +34,10 @@ class State
public function __construct(
array $state,
\DateTimeImmutable $createdAt = null,
DateTimeImmutable $createdAt = null,
HelpersManager $helpersManager = null
) {
$this->createdAt = $createdAt ?? new \DateTimeImmutable();
$this->createdAt = $createdAt ?? new DateTimeImmutable();
$this->helpersManager = $helpersManager ?? new HelpersManager();
$this->identityProviderMetadata = $this->resolveIdentityProviderMetadata($state);
......@@ -105,12 +106,12 @@ class State
return null;
}
public function getCreatedAt(): \DateTimeImmutable
public function getCreatedAt(): DateTimeImmutable
{
return $this->createdAt;
}
protected function resolveAuthenticationInstant(array $state): ?\DateTimeImmutable
protected function resolveAuthenticationInstant(array $state): ?DateTimeImmutable
{
if (empty($state[self::KEY_AUTHENTICATION_INSTANT])) {
return null;
......@@ -119,8 +120,8 @@ class State
$authInstant = (string)$state[self::KEY_AUTHENTICATION_INSTANT];
try {
return new \DateTimeImmutable('@' . $authInstant);
} catch (\Throwable $exception) {
return new DateTimeImmutable('@' . $authInstant);
} catch (Throwable $exception) {
$message = sprintf(
'Unable to create DateTimeImmutable using AuthInstant value \'%s\'. Error was: %s.',
$authInstant,
......@@ -130,7 +131,7 @@ class State
}
}
public function getAuthenticationInstant(): ?\DateTimeImmutable
public function getAuthenticationInstant(): ?DateTimeImmutable
{
return $this->authenticationInstant;
}
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Entities\Bases;
use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
......
......@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Entities;
use DateTimeImmutable;
/**
* Represents a Service Provider to which a user has authenticated at least once.
*/
......@@ -11,23 +13,22 @@ class ConnectedServiceProvider
{
protected ServiceProvider $serviceProvider;
protected int $numberOfAuthentications;
protected \DateTimeImmutable $lastAuthenticationAt;
protected \DateTimeImmutable $firstAuthenticationAt;
protected DateTimeImmutable $lastAuthenticationAt;
protected DateTimeImmutable $firstAuthenticationAt;
protected User $user;
/**
* TODO mivanci make sortable by name (or entity ID if not present), number of authns, last/first authn.
* @param ServiceProvider $serviceProvider
* @param int $numberOfAuthentications
* @param \DateTimeImmutable $lastAuthenticationAt
* @param \DateTimeImmutable $firstAuthenticationAt
* @param DateTimeImmutable $lastAuthenticationAt
* @param DateTimeImmutable $firstAuthenticationAt
* @param User $user
*/
public function __construct(
ServiceProvider $serviceProvider,
int $numberOfAuthentications,
\DateTimeImmutable $lastAuthenticationAt,
\DateTimeImmutable $firstAuthenticationAt,
DateTimeImmutable $lastAuthenticationAt,
DateTimeImmutable $firstAuthenticationAt,
User $user
) {
$this->serviceProvider = $serviceProvider;
......@@ -54,17 +55,17 @@ class ConnectedServiceProvider
}
/**
* @return \DateTimeImmutable
* @return DateTimeImmutable
*/
public function getLastAuthenticationAt(): \DateTimeImmutable
public function getLastAuthenticationAt(): DateTimeImmutable
{
return $this->lastAuthenticationAt;
}
/**
* @return \DateTimeImmutable
* @return DateTimeImmutable
*/
public function getFirstAuthenticationAt(): \DateTimeImmutable
public function getFirstAuthenticationAt(): DateTimeImmutable
{
return $this->firstAuthenticationAt;
}
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Exceptions;
class Exception extends \Exception
......
......@@ -4,16 +4,19 @@ declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Helpers;
use DateInterval;
use DateTimeImmutable;
class DateTimeHelper
{
/**
* Convert date interval to seconds, interval being minimum 1 second.
* @param \DateInterval $dateInterval Minimum is 1 second.
* @param DateInterval $dateInterval Minimum is 1 second.
* @return int
*/
public function convertDateIntervalToSeconds(\DateInterval $dateInterval): int
public function convertDateIntervalToSeconds(DateInterval $dateInterval): int
{
$reference = new \DateTimeImmutable();
$reference = new DateTimeImmutable();
$endTime = $reference->add($dateInterval);
$duration = $endTime->getTimestamp() - $reference->getTimestamp();
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Helpers;
class EnvironmentHelper
......
......@@ -10,6 +10,9 @@ use SimpleSAML\Module\accounting\Exceptions\Exception;
use SimpleSAML\Module\accounting\Exceptions\UnexpectedValueException;
use SimpleSAML\Module\accounting\Interfaces\BuildableUsingModuleConfigurationInterface;
use SimpleSAML\Module\accounting\ModuleConfiguration;
use Throwable;
use function sprintf;
class InstanceBuilderUsingModuleConfigurationHelper
{
......@@ -37,8 +40,8 @@ class InstanceBuilderUsingModuleConfigurationHelper
$reflectionMethod = new ReflectionMethod($class, $method);
/** @var BuildableUsingModuleConfigurationInterface $instance */
$instance = $reflectionMethod->invoke(null, ...$allArguments);
} catch (\Throwable $exception) {
$message = \sprintf(
} catch (Throwable $exception) {
$message = sprintf(
'Error building instance using module configuration. Error was: %s.',
$exception->getMessage()
);
......
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Helpers;
use SimpleSAML\Error\CriticalConfigurationError;
use SimpleSAML\Module\accounting\Exceptions\InvalidConfigurationException;
use SimpleSAML\Module\accounting\ModuleConfiguration;
use SimpleSAML\Utils\HTTP;
......@@ -20,7 +24,12 @@ class ModuleRoutesHelper
public function getUrl(string $path, array $parameters = []): string
{
$url = $this->sspHttpUtils->getBaseURL() . 'module.php/' . ModuleConfiguration::MODULE_NAME . '/' . $path;
try {
$url = $this->sspHttpUtils->getBaseURL() . 'module.php/' . ModuleConfiguration::MODULE_NAME . '/' . $path;
} catch (CriticalConfigurationError $exception) {
$message = \sprintf('Could not load SimpleSAMLphp base URL. Error was: %s', $exception->getMessage());
throw new InvalidConfigurationException($message, (int)$exception->getCode(), $exception);
}
if (!empty($parameters)) {
$url = $this->sspHttpUtils->addURLParameters($url, $parameters);
......
......@@ -4,6 +4,8 @@ declare(strict_types=1);
namespace SimpleSAML\Module\accounting\Helpers;
use Throwable;