-
Valentin Pocotilenco authoredValentin Pocotilenco authored
EarcUtils.php 21.38 KiB
<?php
namespace app\Libraries;
use SimpleSAML\Configuration;
use SimpleSAML\Module\metarefresh\MetaLoader;
class EarcUtils
{
/**
* Return user friendly name of checked categories
*
* @param mixed $value
*
* @return string
*/
public function getEncatName($value)
{
switch ($value) {
case 'http://www.geant.net/uri/dataprotection-code-of-conduct/v1':
$ret = 'Code of Conduct';
break;
case 'http://refeds.org/category/research-and-scholarship':
$ret = 'Research & Scholarship';
break;
default:
$ret = 'None';
}
return $ret;
}
/**
* Retrieve metadata of SP entity by ID from metadata files in SimpleSAMLphp
*
* @param mixed $sp_entity_id_input
*
* @return array
*/
public static function getSpMetadata($sp_entity_id_input)
{
include Configuration::getInstance()->getPathValue('attributenamemapdir', 'attributemap/').'oid2name.php';
include Configuration::getInstance()->getPathValue('metadatadir', 'metadata/').'saml20-sp-remote.php';
if (null != $sp_entity_id_input) {
$sps = array($sp_entity_id_input);
} else {
$sps = config('app.earc_sps');
}
foreach ($sps as $key => $sp_entity_id ) {
if (array_key_exists($sp_entity_id, $metadata)) {
$sp_metadata[$sp_entity_id] = array(
'entityid' => $sp_entity_id,
'name' => $metadata[$sp_entity_id]['name']['en'] ?? 'Not found',
);
if (array_key_exists('EntityAttributes', $metadata[$sp_entity_id])) {
$sp_metadata[$sp_entity_id]['EntityAttributes'] = $metadata[$sp_entity_id]['EntityAttributes'];
}
if (array_key_exists('attributes', $metadata[$sp_entity_id])) {
$sp_metadata[$sp_entity_id]['attributes'] = $metadata[$sp_entity_id]['attributes'];
$sp_metadata[$sp_entity_id]['attributesRequired'] = array_unique($metadata[$sp_entity_id]['attributes.required']);
foreach ($metadata[$sp_entity_id]['attributes'] as $oid) {
$sp_metadata[$sp_entity_id]['attributeNames'][] = $attributemap[$oid];
}
foreach ($sp_metadata[$sp_entity_id]['attributesRequired'] as $oid) {
$sp_metadata[$sp_entity_id]['attributeNamesRequired'][] = $attributemap[$oid];
}
} elseif (self::isRnsIndicated($sp_metadata[$sp_entity_id])) {
$sp_metadata[$sp_entity_id]['attributeNamesRequired'] = array('eduPersonPrincipalName', 'mail', 'displayName', 'givenName', 'sn');
$sp_metadata[$sp_entity_id]['attributeNames'] = array('eduPersonPrincipalName', 'mail', 'displayName','eduPersonScopedAffiliation', 'eduPersonTargetedID');
} else {
$sp_metadata[$sp_entity_id]['attributeNames'] = [];
$sp_metadata[$sp_entity_id]['attributeNamesRequired'] = [];
}
}
}
if (null != $sp_entity_id_input) {
return $sp_metadata[$sp_entity_id_input];
} else {
return $sp_metadata ?? [];
}
}
/**
* Parse attribute OID to hunam readable attributes
*
* @return array
*/
public static function getAttributeMap()
{
include Configuration::getInstance()->getPathValue('attributenamemapdir', 'attributemap/').'oid2name.php';
return $attributemap;
}
/**
* Retrieve metadata of IdP entity by ID from metadata files in SimpleSAMLphp
*
* @param mixed $entityid
*
* @return array
*/
public static function getIdpMetadata($entityid)
{
$metaloader = new MetaLoader(null);
// The entity ID must not be url encoded for SSP library to loaded from URL source.
$metaloader->loadSource(array('src' => $entityid));
$metaloader->writeMetadataFiles(Configuration::getInstance()->getPathValue('metadatagenerateddir', 'metadata-generated-idp/'));
include Configuration::getInstance()->getPathValue('metadatagenerateddir', 'metadata-generated-idp/').'saml20-idp-remote.php';
return $metadata[$entityid];
}
/**
* Get IdP Name from metadata files in SimpleSAMLphp
*
* @param mixed $entityid
*
* @return string
*/
public static function getIdentityProviderName($entityid)
{
$idp_metadata = self::getIdpMetadata($entityid);
return $idp_metadata['name']['en'];
}
public static function getRegistrationAuthority($idp_metadata)
{
$decoded = base64_decode($idp_metadata['entityDescriptor']);
if (preg_match('/registrationAuthority=\"(.*)\"/U', $decoded, $match)) {
return $match[1];
} else {
return;
}
}
/**
* Check research-and-scholarship support on IdP entity
*
* @param mixed $idp_metadata
*
* @return bool
*/
public static function isRnsSupportIndicated($idp_metadata)
{
$ret = false;
if (array_key_exists('EntityAttributes', $idp_metadata) && array_key_exists('http://macedir.org/entity-category-support', $idp_metadata['EntityAttributes']) && is_array($idp_metadata['EntityAttributes']['http://macedir.org/entity-category-support'])) {
foreach ($idp_metadata['EntityAttributes']['http://macedir.org/entity-category-support'] as $encat) {
if ($encat === 'http://refeds.org/category/research-and-scholarship') {
$ret = true;
}
}
}
return $ret;
}
/**
* Check research-and-scholarship support on SP entity
*
* @param mixed $sp_metadata
*
* @return bool
*/
public static function isRnsIndicated($sp_metadata)
{
$ret = false;
if (array_key_exists('EntityAttributes', $sp_metadata) && array_key_exists('http://macedir.org/entity-category', $sp_metadata['EntityAttributes']) && is_array($sp_metadata['EntityAttributes']['http://macedir.org/entity-category'])) {
foreach ($sp_metadata['EntityAttributes']['http://macedir.org/entity-category'] as $encat) {
if ($encat === 'http://refeds.org/category/research-and-scholarship') {
$ret = true;
}
}
}
return $ret;
}
/**
* Check eduPerson mail based or schacHomeOrganizationType attributes returned value
*
* @param mixed $attributeName
* @param mixed $attributeValue
*
* @return mixed
*/
public static function checkAttributeSyntax($attributeName, $attributeValue)
{
$failed = array();
if (in_array($attributeName, array('eduPersonPrincipalName', 'mail', 'eduPersonScopedAffiliation'))) {
if (!filter_var($attributeValue, FILTER_VALIDATE_EMAIL)) {
$failed[] = $attributeName;
}
} elseif ($attributeName == 'schacHomeOrganizationType') {
if (!preg_match('/urn:schac:homeOrganizationType:/', $attributeValue)) {
$failed[] = $attributeName;
}
}
if (!empty($failed)) {
return implode(',', $failed);
} else {
return true;
}
}
/**
* Check minimal sufficient set of released attributes
*
* @param mixed $released_attributes
*
* @return bool
*/
public static function isMinimalSubsetSent($released_attributes)
{
$minimalSubset = array('eduPersonPrincipalName', 'mail', 'displayName');
foreach ($minimalSubset as $attributeName) {
if (!array_key_exists($attributeName, $released_attributes) && !self::canBeRedundant($attributeName, $released_attributes)) {
return false;
}
}
return true;
}
/**
* Check basic set of released attributes
*
* @param mixed $released_attributes
*
* @return bool
*/
public static function isBasicSubsetSent($released_attributes)
{
$minimalSubset = array('eduPersonPrincipalName', 'eduPersonTargetedID', 'eduPersonUniqueId');
foreach ($minimalSubset as $attributeName) {
if (array_key_exists($attributeName, $released_attributes)) {
return true;
}
}
return false;
}
/**
* Check attributes redundancy
*
* @param mixed $attributeName
* @param mixed $released_attributes
*
* @return bool
*/
public static function canBeRedundant($attributeName, $released_attributes)
{
if (($attributeName == 'schacHomeOrganization' && (array_key_exists('eduPersonPrincipalName', $released_attributes) || array_key_exists('eduPersonScopedAffiliation', $released_attributes))) ||
($attributeName == 'cn' && ((array_key_exists('sn', $released_attributes) && array_key_exists('givenName', $released_attributes)) || array_key_exists('displayName', $released_attributes))) ||
($attributeName == 'eduPersonAffiliation' && (array_key_exists('eduPersonScopedAffiliation', $released_attributes))) ||
($attributeName == 'sn' && (array_key_exists('displayName', $released_attributes) || array_key_exists('cn', $released_attributes))) ||
($attributeName == 'givenName' && (array_key_exists('displayName', $released_attributes) || array_key_exists('cn', $released_attributes))) ||
($attributeName == 'displayName' && ((array_key_exists('sn', $released_attributes) && array_key_exists('givenName', $released_attributes)) || array_key_exists('cn', $released_attributes)))
) {
return true;
} else {
return false;
}
}
/**
* Additional analysis of released attributes for more precise evaluation
*
* @param mixed $mark
* @param mixed $sp
* @param mixed $released_attributes
* @param mixed $superfluous_attributes
* @param mixed $idp_metadata
* @param mixed $non_personal_attributes
*
* @return string
*/
public static function getExtraPoints($mark, $sp, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes)
{
$ret = '';
switch ($mark) {
case 'A':
if (self::isRnsSupportIndicated($idp_metadata)) {
$ret = '+';
} else {
$ret = '-';
}
break;
default:
// IdP sends superfluous non-personal information
if (count($superfluous_attributes) > 0) {
foreach ($superfluous_attributes as $attributeName) {
if (in_array($attributeName, $non_personal_attributes)) {
$ret = '-';
break;
}
}
}
// Redundant attributes are missing, but information is available
foreach ($sp['attributeNames'] as $attributeName) {
if (!array_key_exists($attributeName, $released_attributes) && self::canBeRedundant($attributeName, $released_attributes)) {
$ret = '-';
break;
}
}
break;
}
return $ret;
}
/**
* Handle the case where the IdP releases the same value in SAML NameID and
* and in eduPersonTargetedID.
*
* @param array $released_attributes
*
* @return array
*/
public static function handleEptid($released_attributes)
{
// The attribute is decoded as eduPersonTargetedID
// The NameID value is decoded as persistent-id
if (array_key_exists('eduPersonTargetedID', $released_attributes) && array_key_exists('persistent-id', $released_attributes)) {
if ($released_attributes['eduPersonTargetedID'] == $released_attributes['persistent-id']) {
unset($released_attributes['persistent-id']);
}
return $released_attributes;
} elseif (!array_key_exists('eduPersonTargetedID', $released_attributes) && array_key_exists('persistent-id', $released_attributes)) {
$released_attributes['eduPersonTargetedID'] = $released_attributes['persistent-id'];
unset($released_attributes['persistent-id']);
return $released_attributes;
} elseif (array_key_exists('eduPersonTargetedID', $released_attributes) && !array_key_exists('persistent-id', $released_attributes)) {
//This should give an error ?
return $released_attributes;
} else {
return $released_attributes;
}
}
/**
* Released attributes analysis and verdict calculation
*
* @param mixed $sp_entity_id
* @param mixed $released_attributes
* @param mixed $idp_entityid
*
* @return array
*/
public static function calculateVerdictForAnSP($sp_entity_id, $released_attributes, $idp_entityid)
{
$additional_information = array();
$single_valued_attributes = array('eduPersonTargetedID', 'eduPersonPrincipalName', 'sn', 'givenName', 'displayName', 'preferredLanguage', 'schacDateOfBirth', 'schacYearOfBirth', 'schacPersonalTitle', 'eduPersonNickName', 'eduPersonPrimaryOrgUnitDN');
$non_personal_attributes = array('schacHomeOrganization', 'schacHomeOrganizationType', 'eduPersonAffiliation', 'eduPersonScopedAffiliation', 'o', 'swissEduPersonHomeOrganization', 'swissEduPersonHomeOrganizationType');
$rns_attributes = array('eduPersonPrincipalName', 'mail', 'displayName', 'givenName', 'sn', 'eduPersonScopedAffiliation');
$sp_metadata = self::getSpMetadata($sp_entity_id);
if (self::isRnsIndicated($sp_metadata)) {
$superfluous_attributes = array_diff(array_keys($released_attributes), $sp_metadata['attributeNamesRequired']);
$superfluous_attributes = array_diff($superfluous_attributes, $rns_attributes);
} else {
$superfluous_attributes = array_diff(array_keys($released_attributes), $sp_metadata['attributeNamesRequired']);
}
$idp_metadata = self::getIdpMetadata($idp_entityid);
// F: No attributes received
if (count(array_keys($released_attributes)) == 0) {
return array('mark' => 'F', 'text' => 'No attributes received', 'additional_information' => $additional_information);
}
// F: R&S category support is indicated but its requirements are not satisfied
if (self::isRnsSupportIndicated($idp_metadata) && self::isRnsIndicated($sp_metadata) && !self::isMinimalSubsetSent($released_attributes)) {
return array('mark' => 'F', 'text' => 'R&S category support is indicated but its requirements are not satisfied', 'additional_information' => $additional_information);
}
// F: Incorrect value syntax (except for eptid)
foreach ($released_attributes as $attributeName => $attributeValues) {
if (in_array($attributeName, $single_valued_attributes) && count($attributeValues) > 1) {
return array('mark' => 'F', 'text' => 'Incorrect value syntax, got multiple values for a single value attribute', 'additional_information' => $additional_information);
}
if ($attributeName != 'eduPersonTargetedID') {
foreach ($attributeValues as $attributeValue) {
$checkResult = self::checkAttributeSyntax($attributeName, $attributeValue);
if (!$checkResult) {
return array('mark' => 'F', 'text' => 'Incorrect value syntax: '.$checkResult, 'additional_information' => $additional_information);
}
}
}
}
// D: IdP does not release minimal and does not indicate RnS support
if (!self::isMinimalSubsetSent($released_attributes) && self::isRnsSupportIndicated($idp_metadata) && self::isRnsIndicated($sp_metadata)) {
return array('mark' => 'D', 'text' => 'REFEDS R&S Entity Category is not supported. Consider enabling support, more information can be found in <a href="https://wiki.refeds.org/display/ENT/Research+and+Scholarship+FAQ">REFEDS wiki</a>', 'additional_information' => $additional_information);
}
// D: IdP sends some subset of the requested information, but no persistent identifier as specified in basic information
if (!self::isBasicSubsetSent($released_attributes)) {
return array('mark' => 'D', 'text' => 'IdP sends some subset of the requested information, but no persistent identifier as specified in basic information', 'additional_information' => $additional_information);
}
// D: IdP sends superfluous _personal_ information
if (count($superfluous_attributes) > 0) {
foreach ($superfluous_attributes as $attributeName) {
if (!self::canBeRedundant($attributeName, $released_attributes)) {
if (!in_array($attributeName, $non_personal_attributes)) {
if ($attributeName == 'eduPersonEntitlement' && count($released_attributes['eduPersonEntitlement']) == 1 && $released_attributes['eduPersonEntitlement'][0] == 'urn:mace:dir:entitlement:common-lib-terms') {
unset($superfluous_attributes[array_search('eduPersonEntitlement', $superfluous_attributes)]);
$additional_information[] = 'eduPersonEntitlement attribute with "urn:mace:dir:entitlement:common-lib-terms" value has been sent, but it was not required.';
continue;
}
if ($attributeName == 'eduPersonTargetedID') {
unset($superfluous_attributes[array_search('eduPersonTargetedID', $superfluous_attributes)]);
$additional_information[] = 'eduPersonTargetedID has been sent, but it was not required.';
continue;
}
return array('mark' => 'D', 'text' => 'IdP sends superfluous _personal_ information in <b>' . $attributeName . '</b> attribute', 'additional_information' => $additional_information);
}
}
}
}
// C: IdP sends eduPersonTargetedId with the wrong (legacy) syntax
if (array_key_exists('eduPersonTargetedID', $released_attributes)) {
if (filter_var($released_attributes['eduPersonTargetedID'], FILTER_VALIDATE_EMAIL)) {
return array('mark' => 'C'.self::getExtraPoints('C', $sp_metadata, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes), 'text' => 'IdP sends eduPersonTargetedId with the wrong (legacy) syntax', 'additional_information' => $additional_information);
}
}
// C: IdP sends basic information while some required information is missing
if (self::isBasicSubsetSent($released_attributes)) {
foreach ($sp_metadata['attributeNamesRequired'] as $attribute) {
if (!array_key_exists($attribute, $released_attributes) && !self::canBeRedundant($attribute, $released_attributes)) {
if (self::isRnsIndicated($sp_metadata) && $attribute == 'eduPersonTargetedId' && array_key_exists('eduPersonPrincipalName', $released_attributes)) {
$remark = true;
} else {
return array('mark' => 'C'.self::getExtraPoints('C', $sp_metadata, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes), 'text' => 'IdP sends basic information while some required information is missing', 'additional_information' => $additional_information);
}
}
}
}
// B: IdP sends minimal information
if (self::isMinimalSubsetSent(array_keys($released_attributes))) {
foreach ($sp_metadata['attributeNames'] as $attribute) {
if (!array_key_exists($attribute, $released_attributes) && !self::canBeRedundant($attribute, $released_attributes)) {
return array('mark' => 'B'.self::getExtraPoints('B', $sp_metadata, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes), 'text' => 'IdP sends minimal information', 'additional_information' => $additional_information);
}
}
}
// A-: IdP sends some non personal superfluous attributes
if (count($superfluous_attributes) > 0) {
return array('mark' => 'A'.self::getExtraPoints('B', $sp_metadata, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes), 'text' => 'IdP sends all necessary information but sends some non-personal superfluous attributes ('.implode(',', $superfluous_attributes).') as well.', 'additional_information' => $additional_information);
}
// A: IdP sends all necessary information
if (count($superfluous_attributes) == 0) {
return array('mark' => 'A'.self::getExtraPoints('A', $sp_metadata, $released_attributes, $superfluous_attributes, $idp_metadata, $non_personal_attributes), 'text' => 'Great! IdP sends all necessary information', 'additional_information' => $additional_information);
}
}
}