Skip to content
Snippets Groups Projects
Commit 04592cfd authored by Pavel Vyskočil's avatar Pavel Vyskočil
Browse files

chore: merge branch 'eligibility' into 'main'

feat: filters for einfra eligibility

Closes PRX-198

See merge request perun-proxy-aai/simplesamlphp/simplesamlphp-module-cesnet!59
parents e536caff f660efa5
No related branches found
No related tags found
1 merge request!59feat: filters for einfra eligibility
Pipeline #280755 passed with warnings
......@@ -14,6 +14,8 @@ Any change that significantly changes behavior in a backward-incompatible way or
- Auth Process filters:
- computeloa
- iscesneteligible
- iseinfraczeligible
- iseinfraassured
## Instalation
......
......@@ -17,7 +17,8 @@
"ext-curl": "*"
},
"suggest": {
"cesnet/simplesamlphp-module-privacyidea": "included privacyIDEA template is for this module"
"cesnet/simplesamlphp-module-privacyidea": "included privacyIDEA template is for this module",
"ext-yaml": "needed to run IsEinfraCZEligible auth filter"
},
"config": {
"platform": {
......
......@@ -27,3 +27,40 @@ Example how to configure IsCesnetEligible filter:
'LDAP.attributeName' => 'isCesnetEligible',
],
```
## IsEinfraCZEligible
Example how to configure IsEinfraCZEligible filter:
- Configuration file is saml20-idp-hosted.php
- User eligibilites and affiliations should be retrieved before this filter is run
- User ext. sources are not updated as part of this filter
```php
33 => [
'class' => 'cesnet:IsEinfraCZEligible',
'userEligibilityAttrName' => 'session_eligibilities',
'userAffiliationAttrName' => 'externalAffiliation',
'eligibilitiesAttrKey' => 'einfracz',
],
```
## IsEinfraAssured
Example how to configure IsEinfraAssured filter:
- Configuration file is saml20-idp-hosted.php
- Einfracz eligibility should be resolved before this filter is run
- User eligibilities and session eligibilities should be retrieved before this filter is run
- The result values are stored in attributes defined in `userAssuranceAttrNames` as value `assurancePrefix-1y`
```php
34 => [
'class' => 'cesnet:IsEinfraAssured',
'userAssuranceAttrNames' => ['eduPersonAssurance'],
'userEligibilitiesAttrName' => 'eligibilities',
'sessionEligibilitiesAttrName' => 'session_eligibilities'
'eligibilitiesAttrKey' => 'einfracz',
'assurancePrefix' => 'assuranceprefix',
],
```
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\cesnet\Auth\Process;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Configuration;
use SimpleSAML\Logger;
use SimpleSAML\Error\Exception;
/**
* Class IsEinfraAssured.
*
* This class puts assurance timestamp as {prefix}-1y representing
* user's einfra eligibility is not older than 1 year.
*/
class IsEinfraAssured extends ProcessingFilter
{
public const LOGGER_PREFIX = 'cesnet:IsEinfraAssured - ';
public const USER_ASSURANCE_ATTRS = 'userAssuranceAttrNames';
public const USER_ELIGIBILITIES_ATTR = 'userEligibilitiesAttrName';
public const SESSION_ELIGIBILITIES_ATTR = 'sessionEligibilitiesAttrName';
public const ELIGIBILITIES_ATTR_KEY = 'eligibilitiesAttrKey';
public const PREFIX = 'assurancePrefix';
private $userEligibilitiesAttr = 'eligibilities';
private $sessionEligibilitiesAttr = 'session_eligibilities';
private $assuranceAttrs = ['eduPersonAssurance'];
private $prefix = 'prefix';
private const SUFFIX = '-1y';
private $eligibilityKey = 'einfracz';
public function __construct($config, $reserved)
{
parent::__construct($config, $reserved);
$config = Configuration::loadFromArray($config);
if ($config === null) {
throw new Exception(
self::LOGGER_PREFIX . ' configuration is missing or invalid!'
);
}
$this->assuranceAttrs = $config->getArray(self::USER_ASSURANCE_ATTRS, $this->assuranceAttrs);
$this->userEligibilitiesAttr = $config->getString(
self::USER_ELIGIBILITIES_ATTR,
$this->userEligibilitiesAttr
);
$this->sessionEligibilitiesAttr = $config->getString(
self::SESSION_ELIGIBILITIES_ATTR,
$this->sessionEligibilitiesAttr
);
$this->prefix = $config->getString(self::PREFIX, $this->prefix);
$this->eligibilityKey = $config->getString(self::ELIGIBILITIES_ATTR_KEY, $this->eligibilityKey);
if (empty($this->assuranceAttrs)) {
throw new Exception(
self::LOGGER_PREFIX . ' empty array detected !'
);
}
}
public function process(&$request)
{
$timestamp = 0;
if (isset($request['Attributes'][$this->sessionEligibilitiesAttr][$this->eligibilityKey])) {
$timestamp = $request['Attributes'][$this->sessionEligibilitiesAttr][$this->eligibilityKey];
} elseif (isset($request['Attributes'][$this->userEligibilitiesAttr][$this->eligibilityKey])) {
$timestamp = $request['Attributes'][$this->userEligibilitiesAttr][$this->eligibilityKey];
}
if ($timestamp > strtotime('-1 year')) {
foreach ($this->assuranceAttrs as $attr) {
$request['Attributes'][$attr][] = $this->prefix . self::SUFFIX;
}
Logger::debug(
self::LOGGER_PREFIX .
'Added assurance timestamp ' . $this->prefix . self::SUFFIX
);
} else {
Logger::debug(
self::LOGGER_PREFIX .
'User is not assured - assurance timestamp not added.'
);
}
}
}
<?php
declare(strict_types=1);
namespace SimpleSAML\Module\cesnet\Auth\Process;
use SimpleSAML\Auth\ProcessingFilter;
use SimpleSAML\Configuration;
use SimpleSAML\Error\Exception;
use SimpleSAML\Logger;
/**
* Class IsEinfraCZEligible.
*
* This class puts the current timestamp to attribute storing user eligibilities,
* when user's affiliations are sufficient to be eligible, which is checked with
* a configuration that is periodically fetched from configured URL.
*/
class IsEinfraCZEligible extends ProcessingFilter
{
public const LOGGER_PREFIX = 'cesnet:IsEinfraCZEligible - ';
public const IDP_ELIGIBLE_AFFILIATIONS_LIST_LINK =
'https://gitlab.ics.muni.cz/perun-proxy-aai/e-infracz_idps_data/-/raw/main/einfra_isacademic/result.yaml';
public const LINK_REFRESH_RATE_MINS = 15; // older file will be refreshed
public const FILE_MAX_AGE_HOURS = 24; // older file won't be considered valid
public const DATA_FILEPATH = '/tmp/einfra_isacademic_eligible.yml'; // where file will be stored and loaded from
public const USER_ELIGIBILITY_ATTR = 'userEligibilityAttrName';
public const USER_AFFILIATIONS_ATTR = 'userAffiliationAttrName';
public const USER_ELIGIBILITY_KEY = 'eligibilitiesAttrKey';
private $userAffiliationAttr = 'externalAffiliation';
private $userEligibilityAttr = 'session_eligibilities';
private $userEligibilityAttrKey = 'einfracz';
public function __construct($config, $reserved)
{
parent::__construct($config, $reserved);
$config = Configuration::loadFromArray($config);
if ($config === null) {
throw new Exception(
self::LOGGER_PREFIX . ' configuration is missing or invalid!'
);
}
$this->userEligibilityAttr = $config->getString(self::USER_ELIGIBILITY_ATTR, $this->userEligibilityAttr);
$this->userAffiliationAttr = $config->getString(self::USER_AFFILIATIONS_ATTR, $this->userAffiliationAttr);
$this->userEligibilityAttrKey = $config->getString(self::USER_ELIGIBILITY_KEY, $this->userEligibilityAttrKey);
}
public function process(&$request)
{
$userScopedAffiliations = [];
if (isset($request['Attributes'][$this->userAffiliationAttr])) {
$userScopedAffiliations
= $request['Attributes'][$this->userAffiliationAttr];
} else {
Logger::error(
self::LOGGER_PREFIX .
'Attribute with name \'' . $this->userAffiliationAttr . '\' was not received from IdP!'
);
}
$isEligible = $this->isEligible($request['saml:sp:IdP'], $userScopedAffiliations);
Logger::debug(
self::LOGGER_PREFIX . 'User is' . ($isEligible ? ' ' : ' not ') . 'eligible'
);
if ($isEligible) {
$request['Attributes'][$this->userEligibilityAttr][$this->userEligibilityAttrKey] = strval(time());
}
Logger::debug(
self::LOGGER_PREFIX . 'Result eligibilities attribute: ' .
json_encode($request['Attributes'][$this->userEligibilityAttr])
);
}
private function isEligible(string $idpEntityId, array $userScopedAffiliations): bool
{
$userAffiliations = array_map(
function ($item) {
return explode("@", $item)[0];
},
$userScopedAffiliations
);
$allowedAffiliations = $this->getAllowedAffiliations($idpEntityId);
$result = array_intersect($userAffiliations, $allowedAffiliations);
Logger::debug(
self::LOGGER_PREFIX .
'Affiliations that make user eligible: ' . implode(",", $result)
);
return !empty($result);
}
private function getAllowedAffiliations(string $idpEntityId): array
{
$allowedAffiliations = [];
$eligibilities = $this->getEligibilityData();
if (array_key_exists($idpEntityId, $eligibilities)) {
$allowedAffiliations = $eligibilities[$idpEntityId];
}
return $allowedAffiliations;
}
private function getEligibilityData(): array
{
$data = 0;
if (file_exists(self::DATA_FILEPATH)) {
$file_age = strtotime(date("F d Y H:i:s.", filectime(self::DATA_FILEPATH)));
if ($file_age < strtotime("-" . self::LINK_REFRESH_RATE_MINS . " minutes")) {
$data = $this->fetchAffiliationData();
}
if (empty($data) and $file_age > strtotime("-" . self::FILE_MAX_AGE_HOURS . " hours")) {
$data = $this->readDataFromCacheFile();
}
} else {
$data = $this->fetchAffiliationData();
}
return $data;
}
private function readDataFromCacheFile(): array
{
try {
$fileSize = filesize(self::DATA_FILEPATH);
$file = fopen(self::DATA_FILEPATH, "r");
$sourceData = fread($file, $fileSize);
fclose($file);
return yaml_parse($sourceData);
} catch (\Exception $exception) {
throw new \Exception(
self::LOGGER_PREFIX . "Cannot read local stored cache file: "
. self::DATA_FILEPATH
);
}
}
private function fetchAffiliationData(): array
{
$sourceData = file_get_contents(self::IDP_ELIGIBLE_AFFILIATIONS_LIST_LINK);
if (!$sourceData) {
Logger::debug(
self::LOGGER_PREFIX . "Cannot download data from " . self::IDP_ELIGIBLE_AFFILIATIONS_LIST_LINK
);
return [];
}
// Store to file
try {
$file = fopen(self::DATA_FILEPATH, "w");
fwrite($file, $sourceData);
fclose($file);
} catch (\Exception $exception) {
Logger::debug(
self::LOGGER_PREFIX . "Cannot store data to file" . self::DATA_FILEPATH
);
}
return yaml_parse($sourceData);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment