Skip to content
Snippets Groups Projects
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);
        }
    }
}