Skip to content
Snippets Groups Projects
Select Git revision
  • 8a11719cea33536e92d083664caf2c5a61d5312f
  • master default protected
  • cesnet_simplesamlphp-1.19.8
  • elixir_simplesamlphp-1.19.8
  • simplesamlphp-1.19.8
  • cesnet_simplesamlphp-1.19.5
  • simplesamlphp-2.0
  • feature/assets
  • feature/rac-source-selector
  • cleanup/remove-base64-attributes
  • simplesamlphp-1.19
  • elixir_simplesamlphp-1.19.5
  • aarc_idp_hinting
  • feature/validate-authstate-before-processing
  • feature/build-two-tarballs
  • dependabot/composer/twig/twig-3.4.3
  • tvdijen-patch-1
  • unchanged-acs-url-no-www-script
  • feature/translation-improvements
  • symfony6
  • move_tests
  • v1.19.9
  • v2.1.3
  • v2.0.10
  • v2.1.2
  • v2.0.9
  • v2.1.1
  • v2.0.8
  • v2.1.0
  • v2.0.7
  • v2.1.0-rc1
  • v2.0.6
  • v2.0.5
  • 2.0.4-alpha.1
  • v2.0.4-alpha.1
  • v2.0.4
  • v2.0.3
  • v2.0.2
  • v2.0.1-alpha.1
  • v2.0.1
  • v1.19.8
41 results

Consent.php

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    IsCesnetEligible.php 11.43 KiB
    <?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;
    use SimpleSAML\Module;
    use SimpleSAML\Module\perun\Adapter;
    use SimpleSAML\Module\perun\AdapterLdap;
    use SimpleSAML\Module\perun\AdapterRpc;
    use SimpleSAML\Module\perun\ChallengeManager;
    use SimpleSAML\Module\perun\LdapConnector;
    use SimpleSAML\Module\perun\model\User;
    
    /**
     * Class IsCesnetEligible.
     *
     * This class put the timestamp of last login into list of Attributes, when at least one value of attribute
     * 'eduPersonScopedAffiliation' is marked as isCesnetEligible in CESNET LDAP
     */
    class IsCesnetEligible extends ProcessingFilter
    {
        public const CONFIG_FILE_NAME = 'module_cesnet_IsCesnetEligible.php';
    
        public const ORGANIZATION_LDAP_BASE = 'ou=Organizations,o=eduID.cz,o=apps,dc=cesnet,dc=cz';
    
        public const INTERFACE_PROPNAME = 'interface';
    
        public const ATTR_NAME = 'attrName';
    
        public const RPC_ATTRIBUTE_NAME = 'RPC.attributeName';
    
        public const LDAP_ATTRIBUTE_NAME = 'LDAP.attributeName';
    
        public const DEFAULT_ATTR_NAME = 'isCesnetEligibleLastSeen';
    
        public const LDAP = 'LDAP';
    
        public const RPC = 'RPC';
    
        public const SCRIPT_NAME = 'updateIsCesnetEligible';
    
        public const PERUN_USER_AFFILIATIONS_ATTR_NAME = 'perunUserAffiliationsAttrName';
    
        public const PERUN_USER_SPONSORING_ORGANIZATIONS_ATTR_NAME = 'perunUserSponsoringOrganizationsAttrName';
    
        private $cesnetEligibleLastSeenValue;
    
        private $interface = self::RPC;
    
        private $rpcAttrName;
    
        private $ldapAttrName;
    
        private $returnAttrName = self::DEFAULT_ATTR_NAME;
    
        private $userAffiliationsAttrName;
    
        private $userSponsoringOrganizationsAttrName;
    
        private $idpEntityId;
    
        private $eduPersonScopedAffiliation = [];
    
        /**
         * @var LdapConnector
         */
        private $cesnetLdapConnector;
    
        /**
         * @var Adapter
         */
        private $adapter;
    
        /**
         * @var AdapterRpc
         */
        private $rpcAdapter;
    
        public function __construct($config, $reserved)
        {
            parent::__construct($config, $reserved);
            $conf = Configuration::loadFromArray($config);
            if (!isset($config[self::RPC_ATTRIBUTE_NAME]) || empty($config[self::RPC_ATTRIBUTE_NAME])) {
                throw new Exception(
                    'cesnet:IsCesnetEligible - missing mandatory configuration option \'' . self::RPC_ATTRIBUTE_NAME . '\'.'
                );
            }
    
            $this->rpcAttrName = $config[self::RPC_ATTRIBUTE_NAME];
    
            $this->cesnetLdapConnector = (new AdapterLdap(self::CONFIG_FILE_NAME))->getConnector();
            $this->rpcAdapter = Adapter::getInstance(Adapter::RPC);
    
            if (isset($config[self::ATTR_NAME]) && !empty($config[self::ATTR_NAME])) {
                $this->returnAttrName = $config['attrName'];
            }
    
            if (
                isset($config[self::INTERFACE_PROPNAME], $config[self::LDAP_ATTRIBUTE_NAME]) &&
                $config[self::INTERFACE_PROPNAME] === self::LDAP && !empty($config[self::LDAP_ATTRIBUTE_NAME])
            ) {
                $this->interface = $config[self::INTERFACE_PROPNAME];
                $this->ldapAttrName = $config[self::LDAP_ATTRIBUTE_NAME];
                $this->adapter = Adapter::getInstance(Adapter::LDAP);
            } else {
                Logger::warning(
                    'cesnet:IsCesnetEligible - One of ' . self::INTERFACE_PROPNAME . self::LDAP_ATTRIBUTE_NAME .
                    ' is missing or empty. RPC interface will be used'
                );
                $this->adapter = Adapter::getInstance(Adapter::RPC);
            }
    
            $this->userSponsoringOrganizationsAttrName =
                $conf->getString(self::PERUN_USER_SPONSORING_ORGANIZATIONS_ATTR_NAME, null);
            $this->userAffiliationsAttrName = $conf->getString(self::PERUN_USER_AFFILIATIONS_ATTR_NAME, null);
    
            if (!isset($this->userAffiliationsAttrName, $this->userSponsoringOrganizationsAttrName)) {
                Logger::warning(
                    'cesnet:IsCesnetEligible - One of attributes [' . $this->userAffiliationsAttrName . ', ' .
                    $this->userSponsoringOrganizationsAttrName . '] wasn\'t set!'
                );
            }
        }
    
        public function process(&$request)
        {
            if (isset($request['perun']['user'])) {
                $user = $request['perun']['user'];
            } else {
                Logger::debug(
                    'cesnet:IsCesnetEligible - ' .
                    'Request doesn\'t contain User, so attribute \'isCesnetEligible\' won\'t be stored.'
                );
                $user = null;
            }
            $this->idpEntityId = $request['saml:sp:IdP'];
    
            if (isset($request['Attributes']['eduPersonScopedAffiliation'])) {
                $this->eduPersonScopedAffiliation
                    = $request['Attributes']['eduPersonScopedAffiliation'];
            } else {
                Logger::error(
                    'cesnet:IsCesnetEligible - ' .
                    'Attribute with name \'eduPersonScopedAffiliation\' did not received from IdP!'
                );
            }
    
            if (!empty($user)) {
                if ($this->interface === self::LDAP) {
                    $attrs = $this->adapter->getUserAttributes($user, [$this->ldapAttrName]);
                    if (isset($attrs[$this->ldapAttrName][0])) {
                        $this->cesnetEligibleLastSeenValue = $attrs[$this->ldapAttrName][0];
                    }
                } else {
                    $this->cesnetEligibleLastSeenValue = $this->adapter->getUserAttributes(
                        $user,
                        [$this->rpcAttrName]
                    )['value'];
                }
            }
    
            if (!empty($this->eduPersonScopedAffiliation) && $this->isCesnetEligible($user)) {
                $this->cesnetEligibleLastSeenValue = date('Y-m-d H:i:s');
    
                if (!empty($user)) {
                    // Update attribute 'isCesnetEligible' in Perun
    
                    $id = uniqid('', true);
    
                    $challengeManager = new ChallengeManager();
    
                    $data = [
                        'userId' => $user->getId(),
                        'isCesnetEligibleValue' => $this->cesnetEligibleLastSeenValue,
                        'cesnetEligibleLastSeenAttrName' => $this->rpcAttrName,
                    ];
    
                    $token = $challengeManager->generateToken($id, self::SCRIPT_NAME, $data);
    
                    $cmd = 'curl -X POST -H "Content-Type: application/json" -d \'' . json_encode($token) . '\' ' .
                        Module::getModuleURL('cesnet/updateIsCesnetEligible.php') . ' > /dev/null &';
    
                    exec($cmd);
                }
            }
    
            if ($this->cesnetEligibleLastSeenValue !== null) {
                $request['Attributes'][$this->returnAttrName] = [$this->cesnetEligibleLastSeenValue];
                Logger::debug(
                    'cesnet:IsCesnetEligible - Attribute ' . $this->returnAttrName . ' was set to value ' .
                    $this->cesnetEligibleLastSeenValue
                );
            }
    
            $request['Attributes']['isCesnetEligible'] = ['false'];
            if (
                ($this->cesnetEligibleLastSeenValue !== null) && $this->cesnetEligibleLastSeenValue > date(
                    'Y-m-d H:i:s',
                    strtotime('-1 year')
                )
            ) {
                $request['Attributes']['isCesnetEligible'] = ['true'];
                Logger::debug('cesnet:IsCesnetEligible - Attribute isCesnetEligible was set to true.');
            }
        }
    
        /**
         * Returns true if one of user's affiliation is in allowed affiliations for this IdP , False if not.
         *
         * @param User $user or Null
         */
        private function isCesnetEligible($user): bool
        {
            $allowedAffiliations = $this->getAllowedAffiliations([$this->idpEntityId]);
            if ($this->compareAffiliations($this->eduPersonScopedAffiliation, $allowedAffiliations)) {
                return true;
            }
    
            // Check if user has isCesnetEligible by sponsoring in some organization
            try {
                if (isset($user, $this->userAffiliationsAttrName, $this->userSponsoringOrganizationsAttrName)) {
                    $userAttributes = $this->rpcAdapter->getUserAttributesValues(
                        $user,
                        [$this->userAffiliationsAttrName, $this->userSponsoringOrganizationsAttrName]
                    );
    
                    $perunUserAffiliations = $userAttributes[$this->userAffiliationsAttrName] ?? [];
                    $perunUserSponsoringOrganizations = $userAttributes[$this->userSponsoringOrganizationsAttrName] ?? [];
    
                    if (empty($perunUserAffiliations) || empty($perunUserSponsoringOrganizations)) {
                        Logger::debug(
                            'cesnet:IsCesnetEligible - One of attributes [' . $this->userAffiliationsAttrName . ':' .
                            json_encode($perunUserAffiliations) . ', ' . $this->userSponsoringOrganizationsAttrName .
                            ':' . json_encode($perunUserSponsoringOrganizations) . '] has empty value!'
                        );
    
                        return false;
                    }
    
                    $allowedSponsoredAffiliations = $this->getAllowedAffiliations($perunUserSponsoringOrganizations);
    
                    return $this->compareAffiliations($perunUserAffiliations, $allowedSponsoredAffiliations);
                }
            } catch (\Exception $exception) {
                Logger::error(
                    'cesnet:IsCesnetEligible - Exception ' . $exception .
                    ' during computing isCesnetEligible by sponsoring in some organizations'
                );
            }
    
            return false;
        }
    
        /**
         * Return list of allowed affiliations for IdP from CESNET LDAP.
         *
         * @param array $idpEntityIds of entityId of IdPs
         *
         * @return array of allowed affiliations
         */
        private function getAllowedAffiliations($idpEntityIds): array
        {
            $allowedAffiliations = [];
            try {
                $filter = '(|';
                foreach ($idpEntityIds as $idpEntityId) {
                    $filter .= '(entityIDofIdP=' . $idpEntityId . ')';
                }
                $filter .= ')';
    
                $results = $this->cesnetLdapConnector->searchForEntities(
                    self::ORGANIZATION_LDAP_BASE,
                    $filter,
                    ['cesnetcustomeraffiliation', 'eduIDczScope']
                );
    
                if (empty($results)) {
                    Logger::debug('cesnet:IsCesnetEligible - Received empty response from LDAP for filter'
                        . $filter . '.');
                } else {
                    foreach ($results as $result) {
                        $affiliations = $result['cesnetcustomeraffiliation'] ?? [];
                        $scopes = $result['eduIDczScope'] ?? [];
    
                        foreach ($scopes as $scope) {
                            foreach ($affiliations as $affiliation) {
                                $allowedAffiliations[] = trim($affiliation) . '@' . trim($scope);
                            }
                        }
                    }
                }
            } catch (Exception $ex) {
                Logger::warning('cesnet:IsCesnetEligible - Unable to connect to LDAP!');
            }
    
            return $allowedAffiliations;
        }
    
        /**
         * Compare two lists of affiliations and returns true if one of affiliations without scope is in booth lists.
         *
         * @param array $userAffiliations    of user scoped affiliations
         * @param array $allowedAffiliations of allowed unscoped affiliations
         */
        private function compareAffiliations($userAffiliations, $allowedAffiliations): bool
        {
            $result = array_intersect($userAffiliations, $allowedAffiliations);
            if (!empty($result)) {
                return true;
            }
    
            return false;
        }
    }