From 6a30edd98976af52ded891c1124d222e739c925a Mon Sep 17 00:00:00 2001 From: Guy Halse <ghalse@users.noreply.github.com> Date: Mon, 14 May 2018 14:09:28 +0200 Subject: [PATCH] Add authproc filter to test correct cardinality (#848) * Add authproc filter to test correct cardinality Attribute schemas often have specific requirements for the number of values a particular attribute may have, and violating the cardinality rules associated with these may have unexpected and undesirable results. The two authproc filters this change introduces allow the cardinality of attributes to be tested. core:Cardinality is the more general case, and allows for both a minimum and maximum number of values to be tested. Processing will abort with an error if too few or too many are received. core:CardinalitySingle is a special case of this for single-valued attributes, and allows various corrective actions to be taken rather than aborting. This allows for cardinality mistakes to be "fixed" in a predictable way, and can also be used to construct single-valued attributes from multi-valued ones. * Fix typo * Fix tests * Added Dutch translations * Fix phpdoc * Whitespace fixes This commit consists of patches automatically generated for this project on https://scrutinizer-ci.com * Fix typo in fixing typo --- .../dictionaries/cardinality.definition.json | 14 ++ .../dictionaries/cardinality.translation.json | 15 ++ modules/core/docs/authproc_cardinality.md | 47 ++++ .../core/docs/authproc_cardinalitysingle.md | 88 +++++++ modules/core/lib/Auth/Process/Cardinality.php | 164 ++++++++++++ .../lib/Auth/Process/CardinalitySingle.php | 109 ++++++++ .../core/templates/cardinality_error.tpl.php | 37 +++ modules/core/www/cardinality_error.php | 25 ++ .../Auth/Process/CardinalitySingleTest.php | 141 +++++++++++ .../core/lib/Auth/Process/CardinalityTest.php | 235 ++++++++++++++++++ 10 files changed, 875 insertions(+) create mode 100644 modules/core/dictionaries/cardinality.definition.json create mode 100644 modules/core/dictionaries/cardinality.translation.json create mode 100644 modules/core/docs/authproc_cardinality.md create mode 100644 modules/core/docs/authproc_cardinalitysingle.md create mode 100644 modules/core/lib/Auth/Process/Cardinality.php create mode 100644 modules/core/lib/Auth/Process/CardinalitySingle.php create mode 100644 modules/core/templates/cardinality_error.tpl.php create mode 100644 modules/core/www/cardinality_error.php create mode 100644 tests/modules/core/lib/Auth/Process/CardinalitySingleTest.php create mode 100644 tests/modules/core/lib/Auth/Process/CardinalityTest.php diff --git a/modules/core/dictionaries/cardinality.definition.json b/modules/core/dictionaries/cardinality.definition.json new file mode 100644 index 000000000..b9059cb93 --- /dev/null +++ b/modules/core/dictionaries/cardinality.definition.json @@ -0,0 +1,14 @@ +{ + "cardinality_header": { + "en": "Incorrect Attributes" + }, + "cardinality_text": { + "en": "One or more of the attributes supplied by your identity provider did not contain the expected number of values." + }, + "problematic_attributes": { + "en": "The problematic attribute(s) are:" + }, + "got_want": { + "en": "got %GOT% values, want %WANT%" + } +} diff --git a/modules/core/dictionaries/cardinality.translation.json b/modules/core/dictionaries/cardinality.translation.json new file mode 100644 index 000000000..d54ab371f --- /dev/null +++ b/modules/core/dictionaries/cardinality.translation.json @@ -0,0 +1,15 @@ +{ + "cardinality_header": { + "af": "Onjuiste Eienskappe", + "nl": "Niet de juiste attributen" + }, + "cardinality_text": { + "nl": "Één of meer door de Identity Provider geleverde attributen bevat niet het vereiste aantal attributen." + }, + "problematic_attributes": { + "nl": "De onjuiste attributen zijn:" + }, + "got_want": { + "nl": "%GOT% ontvangen waarden, %WANT% vereist" + } +} diff --git a/modules/core/docs/authproc_cardinality.md b/modules/core/docs/authproc_cardinality.md new file mode 100644 index 000000000..0902bf278 --- /dev/null +++ b/modules/core/docs/authproc_cardinality.md @@ -0,0 +1,47 @@ +`core:Cardinality` +================== + +Ensure the number of attribute values is within the specified multiplicity. + +This filter should contain a set of attribute name => rule pairs describing the multiplicity rules for an attribute. + +The special parameter `%ignoreEntities` can be used to give an array of entity IDs that should be ignored for testing, etc purposes. + +Specifying Rules +---------------- + +Multiplicity rules are specified as an associative array containing one or more of the following parameters: + +`min` +: The minimum number of values (participation) this attribute should have. Defaults to `zero`. + +`max` +: The maximum number of values (cardinality) this attribute should have. Defaults to no upper bound. + +`warn` +: Log a warning rather than generating an error. Defaults to `false`. + +For convenience, minimum and maximum values can also be specified using a shorthand list notation. + +Examples +-------- + +Require at least one `givenName`, no more than two email addresses, and between two and four values for `eduPersonScopedAffiliation`. + + 'authproc' => array( + 50 => array( + 'class' => 'core:Cardinality', + 'givenName' => array('min' => 1), + 'mail' => array('max' => 2), + 'eduPersonScopedAffiliation' => array('min' => 2, 'max' => 4), + ), + ), + +Use the shorthand notation for min, max: + + 'authproc' => array( + 50 => array( + 'class' => 'core:Cardinality', + 'mail' => array(0, 2), + ), + ), diff --git a/modules/core/docs/authproc_cardinalitysingle.md b/modules/core/docs/authproc_cardinalitysingle.md new file mode 100644 index 000000000..4a61bdbc5 --- /dev/null +++ b/modules/core/docs/authproc_cardinalitysingle.md @@ -0,0 +1,88 @@ +`core:CardinalitySingle` +======================== + +Ensure the correct cardinality of single-valued attributes. This filter is a special case +of the more generic [core:Cardinality] filter that allows for optional corrective measures +when multi-valued attributes are received where single-valued ones are expected. + +Parameters +---------- + +This filter implements a number of optional parameters: + +`singleValued` +: array of attribute names that *must* be single-valued, or a 403 error is generated. + +`firstValue` +: array of attribute names where only the first value of a multi-valued assertion should be returned. + +`flatten` +: array of attribute names where a multi-valued assertion is flattened into a single delimited string. + +`flattenWith` +: the delimiter for `flatten`. Defaults to ";". + +`ignoreEntities` +: array of entity IDs that should be ignored for testing, etc purposes. + +When the same attribute name appears in multiple stanzas, they are processed in the order above. + +Examples +-------- + +Abort with an error if any attribute defined as single-valued in the eduPerson or SCHAC schemas exists and has more than one value: + + 'authproc' => array( + 50 => array( + 'class' => 'core:CardinalitySingle', + 'singleValued' => array( + /* from eduPerson (internet2-mace-dir-eduperson-201602) */ + 'eduPersonOrgDN', 'eduPersonPrimaryAffiliation', 'eduPersonPrimaryOrgUnitDN', + 'eduPersonPrincipalName', 'eduPersonUniqueId', + /* from inetOrgPerson (RFC2798), referenced by internet2-mace-dir-eduperson-201602 */ + 'displayName', 'preferredLanguage', + /* from SCHAC-IAD Version 1.3.0 */ + 'schacMotherTongue', 'schacGender', 'schacDateOfBirth', 'schacPlaceOfBirth', + 'schacPersonalTitle', 'schacHomeOrganization', 'schacHomeOrganizationType', + 'schacExpiryDate', + ), + ), + ), + +Abort if multiple values are received for `eduPersonPrincipalName`, but take the first value for `eduPersonPrimaryAffiliation`: + + 'authproc' => array( + 50 => array( + 'class' => 'core:CardinalitySingle', + 'singleValued' => array('eduPersonPrincipalName'), + 'firstValue' => array('eduPersonPrimaryAffiliation'), + ), + ), + ), + +Construct `eduPersonPrimaryAffiliation` using the first value in `eduPersonAffiliation`: + + 'authproc' => array( + 50 => array( + 'class' => 'core:AttributeCopy', + 'eduPersonAffiliation' => 'eduPersonPrimaryAffiliation', + ), + 51 => array( + 'class' => 'core:CardinalitySingle', + 'firstValue' => array('eduPersonPrimaryAffiliation'), + ), + ), + +Construct a single, comma-separated value version of `eduPersonAffiliation`: + + 'authproc' => array( + 50 => array( + 'class' => 'core:AttributeCopy', + 'eduPersonAffiliation' => 'eduPersonAffiliationWithCommas', + ), + 51 => array( + 'class' => 'core:CardinalitySingle', + 'flatten' => array('eduPersonAffiliationWithCommas'), + 'flattenWith' => ',', + ), + ), diff --git a/modules/core/lib/Auth/Process/Cardinality.php b/modules/core/lib/Auth/Process/Cardinality.php new file mode 100644 index 000000000..74d330f00 --- /dev/null +++ b/modules/core/lib/Auth/Process/Cardinality.php @@ -0,0 +1,164 @@ +<?php + +/** + * Filter to ensure correct cardinality of attributes + * + * @author Guy Halse, http://orcid.org/0000-0002-9388-8592 + * @package SimpleSAMLphp + */ +class sspmod_core_Auth_Process_Cardinality extends SimpleSAML_Auth_ProcessingFilter +{ + /** @var array Associative array with the mappings of attribute names. */ + private $cardinality = array(); + + /** @var array Entities that should be ignored */ + private $ignoreEntities = array(); + + /** + * Initialize this filter, parse configuration. + * + * @param array $config Configuration information about this filter. + * @param mixed $reserved For future use. + * @throws SimpleSAML_Error_Exception + */ + public function __construct($config, $reserved) + { + parent::__construct($config, $reserved); + assert(is_array($config)); + + foreach ($config as $attribute => $rules) { + if ($attribute === '%ignoreEntities') { + $this->ignoreEntities = $config['%ignoreEntities']; + continue; + } + + if (!is_string($attribute)) { + throw new SimpleSAML_Error_Exception('Invalid attribute name: '.var_export($attribute, true)); + } + $this->cardinality[$attribute] = array('warn' => false); + + /* allow either positional or name-based parameters */ + if (isset($rules[0])) { + $this->cardinality[$attribute]['min'] = $rules[0]; + } elseif (isset($rules['min'])) { + $this->cardinality[$attribute]['min'] = $rules['min']; + } + if (isset($rules[1])) { + $this->cardinality[$attribute]['max'] = $rules[1]; + } elseif (isset($rules['max'])) { + $this->cardinality[$attribute]['max'] = $rules['max']; + } + if (array_key_exists('warn', $rules)) { + $this->cardinality[$attribute]['warn'] = (bool) $rules['warn']; + } + + /* sanity check the rules */ + if (!array_key_exists('min', $this->cardinality[$attribute])) { + $this->cardinality[$attribute]['min'] = 0; + } elseif (!is_int($this->cardinality[$attribute]['min']) || + $this->cardinality[$attribute]['min'] < 0 + ) { + throw new SimpleSAML_Error_Exception('Minimum cardinality must be a positive integer: '. + var_export($attribute, true)); + } + if (array_key_exists('max', $this->cardinality[$attribute]) && + !is_int($this->cardinality[$attribute]['max']) + ) { + throw new SimpleSAML_Error_Exception('Maximum cardinality must be a positive integer: '. + var_export($attribute, true)); + } + if (array_key_exists('min', $this->cardinality[$attribute]) && + array_key_exists('max', $this->cardinality[$attribute]) && + $this->cardinality[$attribute]['min'] > $this->cardinality[$attribute]['max'] + ) { + throw new SimpleSAML_Error_Exception('Minimum cardinality must be less than maximium: '. + var_export($attribute, true)); + } + + /* generate a display expression */ + $this->cardinality[$attribute]['_expr'] = sprintf('%d ≤ n', $this->cardinality[$attribute]['min']); + if (array_key_exists('max', $this->cardinality[$attribute])) { + $this->cardinality[$attribute]['_expr'] .= sprintf(' ≤ %d', $this->cardinality[$attribute]['max']); + } + } + } + + /** + * Process this filter + * + * @param array &$request The current request + */ + public function process(&$request) + { + assert(is_array($request)); + assert(array_key_exists("Attributes", $request)); + + $entityid = false; + if (array_key_exists('Source', $request) && array_key_exists('entityid', $request['Source'])) { + $entityid = $request['Source']['entityid']; + } + if (in_array($entityid, $this->ignoreEntities)) { + SimpleSAML\Logger::debug('Cardinality: Ignoring assertions from '.$entityid); + return; + } + + foreach ($request['Attributes'] as $k => $v) { + + if (!array_key_exists($k, $this->cardinality)) { + continue; + } + if (!is_array($v)) { + $v = array($v); + } + + /* minimum cardinality */ + if (count($v) < $this->cardinality[$k]['min']) { + if ($this->cardinality[$k]['warn']) { + SimpleSAML\Logger::warning(sprintf( + 'Cardinality: attribute %s from %s does not meet minimum cardinality of %d (%d)', + $k, $entityid, $this->cardinality[$k]['min'], count($v) + )); + } else { + $request['core:cardinality:errorAttributes'][$k] = array(count($v), $this->cardinality[$k]['_expr']); + } + continue; + } + + /* maximum cardinality */ + if (array_key_exists('max', $this->cardinality[$k]) && count($v) > $this->cardinality[$k]['max']) { + if ($this->cardinality[$k]['warn']) { + SimpleSAML\Logger::warning(sprintf( + 'Cardinality: attribute %s from %s does not meet maximum cardinality of %d (%d)', + $k, $entityid, $this->cardinality[$k]['max'], count($v) + )); + } else { + $request['core:cardinality:errorAttributes'][$k] = array(count($v), $this->cardinality[$k]['_expr']); + } + continue; + } + } + + /* check for missing attributes with a minimum cardinality */ + foreach ($this->cardinality as $k => $v) { + if (!$this->cardinality[$k]['min'] || array_key_exists($k, $request['Attributes'])) { + continue; + } + if ($this->cardinality[$k]['warn']) { + SimpleSAML\Logger::warning(sprintf( + 'Cardinality: attribute %s from %s is missing', + $k, $entityid + )); + } else { + $request['core:cardinality:errorAttributes'][$k] = array(0, $this->cardinality[$k]['_expr']); + } + } + + /* abort if we found a problematic attribute */ + if (array_key_exists('core:cardinality:errorAttributes', $request)) { + $id = SimpleSAML_Auth_State::saveState($request, 'core:cardinality'); + $url = SimpleSAML\Module::getModuleURL('core/cardinality_error.php'); + \SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('StateId' => $id)); + return; + } + } +} diff --git a/modules/core/lib/Auth/Process/CardinalitySingle.php b/modules/core/lib/Auth/Process/CardinalitySingle.php new file mode 100644 index 000000000..459d57d8b --- /dev/null +++ b/modules/core/lib/Auth/Process/CardinalitySingle.php @@ -0,0 +1,109 @@ +<?php + +/** + * Filter to ensure correct cardinality of single-valued attributes + * + * This filter implements a special case of the core:Cardinality filter, and + * allows for optional corrections to be made when cardinality errors are encountered. + * + * @author Guy Halse, http://orcid.org/0000-0002-9388-8592 + * @package SimpleSAMLphp + */ +class sspmod_core_Auth_Process_CardinalitySingle extends SimpleSAML_Auth_ProcessingFilter +{ + /** @var array Attributes that should be single-valued or we generate an error */ + private $singleValued = array(); + + /** @var array Attributes for which the first value should be taken */ + private $firstValue = array(); + + /** @var array Attributes that can be flattened to a single value */ + private $flatten = array(); + + /** @var string Separator for flattened value */ + private $flattenWith = ';'; + + /** @var array Entities that should be ignored */ + private $ignoreEntities = array(); + + /** + * Initialize this filter, parse configuration. + * + * @param array $config Configuration information about this filter. + * @param mixed $reserved For future use. + */ + public function __construct($config, $reserved) + { + parent::__construct($config, $reserved); + assert(is_array($config)); + + if (array_key_exists('singleValued', $config)) { + $this->singleValued = $config['singleValued']; + } + if (array_key_exists('firstValue', $config)) { + $this->firstValue = $config['firstValue']; + } + if (array_key_exists('flattenWith', $config)) { + $this->flattenWith = is_array($config['flattenWith']) ? array_shift($config['flattenWith']) : $config['flattenWith']; + } + if (array_key_exists('flatten', $config)) { + $this->flatten = $config['flatten']; + } + if (array_key_exists('ignoreEntities', $config)) { + $this->ignoreEntities = $config['ignoreEntities']; + } + /* for consistency with core:Cardinality */ + if (array_key_exists('%ignoreEntities', $config)) { + $this->ignoreEntities = $config['%ignoreEntities']; + } + } + + /** + * Process this filter + * + * @param array &$request The current request + */ + public function process(&$request) + { + assert(is_array($request)); + assert(array_key_exists("Attributes", $request)); + + if (array_key_exists('Source', $request) && + array_key_exists('entityid', $request['Source']) && + in_array($request['Source']['entityid'], $this->ignoreEntities) + ) { + SimpleSAML\Logger::debug('CardinalitySingle: Ignoring assertions from '.$request['Source']['entityid']); + return; + } + + foreach ($request['Attributes'] as $k => $v) { + if (!is_array($v)) { + continue; + } + if (count($v) <= 1) { + continue; + } + + if (in_array($k, $this->singleValued)) { + $request['core:cardinality:errorAttributes'][$k] = array(count($v), '0 ≤ n ≤ 1'); + continue; + } + if (in_array($k, $this->firstValue)) { + $request['Attributes'][$k] = array(array_shift($v)); + continue; + } + if (in_array($k, $this->flatten)) { + $request['Attributes'][$k] = array(implode($this->flattenWith, $v)); + continue; + } + } + + /* abort if we found a problematic attribute */ + if (array_key_exists('core:cardinality:errorAttributes', $request)) { + $id = SimpleSAML_Auth_State::saveState($request, 'core:cardinality'); + $url = SimpleSAML\Module::getModuleURL('core/cardinality_error.php'); + \SimpleSAML\Utils\HTTP::redirectTrustedURL($url, array('StateId' => $id)); + return; + } + } +} diff --git a/modules/core/templates/cardinality_error.tpl.php b/modules/core/templates/cardinality_error.tpl.php new file mode 100644 index 000000000..dc75c540a --- /dev/null +++ b/modules/core/templates/cardinality_error.tpl.php @@ -0,0 +1,37 @@ +<?php +/** + * Template which is shown when when an attribute violates a cardinality rule + * + * Parameters: + * - 'target': Target URL. + * - 'params': Parameters which should be included in the request. + * + * @package SimpleSAMLphp + */ + + +$this->data['cardinality_header'] = $this->t('{core:cardinality:cardinality_header}'); +$this->data['cardinality_text'] = $this->t('{core:cardinality:cardinality_text}'); +$this->data['problematic_attributes'] = $this->t('{core:cardinality:problematic_attributes}'); + +$this->includeAtTemplateBase('includes/header.php'); +?> +<h1><?php echo $this->data['cardinality_header']; ?></h1> +<p><?php echo $this->data['cardinality_text']; ?></p> +<h3><?php echo $this->data['problematic_attributes']; ?></h3> +<dl class="cardinalityErrorAttributes"> +<?php foreach ($this->data['cardinalityErrorAttributes'] as $attr => $v) { ?> + <dt><?php echo $attr ?></td> + <dd><?php echo $this->t('{core:cardinality:got_want}', array('%GOT%' => $v[0], '%WANT%' => htmlspecialchars($v[1]))) ?></dd> + </tr> +<?php } ?> +</dl> +<?php +if (isset($this->data['LogoutURL'])) { +?> +<p><a href="<?php echo htmlspecialchars($this->data['LogoutURL']); ?>"><?php echo $this->t('{status:logout}'); ?></a></p> +<?php +} +?> +<?php +$this->includeAtTemplateBase('includes/footer.php'); diff --git a/modules/core/www/cardinality_error.php b/modules/core/www/cardinality_error.php new file mode 100644 index 000000000..4b36495fb --- /dev/null +++ b/modules/core/www/cardinality_error.php @@ -0,0 +1,25 @@ +<?php +/** + * Show a 403 Forbidden page when an attribute violates a cardinality rule + * + * @package SimpleSAMLphp + */ + +if (!array_key_exists('StateId', $_REQUEST)) { + throw new \SimpleSAML_Error_BadRequest('Missing required StateId query parameter.'); +} +$id = $_REQUEST['StateId']; +$state = \SimpleSAML_Auth_State::loadState($id, 'core:cardinality'); +$session = \SimpleSAML_Session::getSessionFromRequest(); + +\SimpleSAML\Logger::stats('core:cardinality:error '.$state['Destination']['entityid'].' '.$state['saml:sp:IdP']. + ' '.implode(',', array_keys($state['core:cardinality:errorAttributes']))); + +$globalConfig = SimpleSAML_Configuration::getInstance(); +$t = new \SimpleSAML_XHTML_Template($globalConfig, 'core:cardinality_error.tpl.php'); +$t->data['cardinalityErrorAttributes'] = $state['core:cardinality:errorAttributes']; +if (isset($state['Source']['auth'])) { + $t->data['LogoutURL'] = \SimpleSAML\Module::getModuleURL('core/authenticate.php', array('as' => $state['Source']['auth']))."&logout"; +} +header('HTTP/1.0 403 Forbidden'); +$t->show(); diff --git a/tests/modules/core/lib/Auth/Process/CardinalitySingleTest.php b/tests/modules/core/lib/Auth/Process/CardinalitySingleTest.php new file mode 100644 index 000000000..ccc03bf7c --- /dev/null +++ b/tests/modules/core/lib/Auth/Process/CardinalitySingleTest.php @@ -0,0 +1,141 @@ +<?php +// Alias the PHPUnit 6.0 ancestor if available, else fall back to legacy ancestor +if (class_exists('\PHPUnit\Framework\TestCase', true) and !class_exists('\PHPUnit_Framework_TestCase', true)) { + class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase', true); +} + +/** + * Test for the core:CardinalitySingle filter. + */ +class Test_Core_Auth_Process_CardinalitySingleTest extends \PHPUnit_Framework_TestCase +{ + /** + * Helper function to run the filter with a given configuration. + * + * @param array $config The filter configuration. + * @param array $request The request state. + * @return array The state array after processing. + */ + private static function processFilter(array $config, array $request) + { + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $filter = new sspmod_core_Auth_Process_CardinalitySingle($config, null); + $filter->process($request); + return $request; + } + + protected function setUp() + { + \SimpleSAML_Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); + } + + /** + * Test singleValued + */ + public function testSingleValuedUnchanged() + { + $config = array( + 'singleValued' => array('eduPersonPrincipalName') + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('eduPersonPrincipalName' => array('joe@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /** + * Test first value extraction + */ + public function testFirstValue() + { + $config = array( + 'firstValue' => array('eduPersonPrincipalName') + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com', 'bob@example.net'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('eduPersonPrincipalName' => array('joe@example.com')); + $this->assertEquals($expectedData, $attributes, "Only first value should be returned"); + } + + public function testFirstValueUnchanged() + { + $config = array( + 'firstValue' => array('eduPersonPrincipalName') + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('eduPersonPrincipalName' => array('joe@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /** + * Test flattening + */ + public function testFlatten() + { + $config = array( + 'flatten' => array('eduPersonPrincipalName'), + 'flattenWith' => '|', + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com', 'bob@example.net'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('eduPersonPrincipalName' => array('joe@example.com|bob@example.net')); + $this->assertEquals($expectedData, $attributes, "Flattened string should be returned"); + } + + public function testFlattenUnchanged() + { + $config = array( + 'flatten' => array('eduPersonPrincipalName'), + 'flattenWith' => '|', + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('eduPersonPrincipalName' => array('joe@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /** + * Test abort + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessageRegExp /REQUEST_URI/ + */ + public function testAbort() + { + $config = array( + 'singleValued' => array('eduPersonPrincipalName'), + ); + $request = array( + 'Attributes' => array( + 'eduPersonPrincipalName' => array('joe@example.com', 'bob@example.net'), + ), + ); + $result = self::processFilter($config, $request); + } +} diff --git a/tests/modules/core/lib/Auth/Process/CardinalityTest.php b/tests/modules/core/lib/Auth/Process/CardinalityTest.php new file mode 100644 index 000000000..c9df640c6 --- /dev/null +++ b/tests/modules/core/lib/Auth/Process/CardinalityTest.php @@ -0,0 +1,235 @@ +<?php +// Alias the PHPUnit 6.0 ancestor if available, else fall back to legacy ancestor +if (class_exists('\PHPUnit\Framework\TestCase', true) and !class_exists('\PHPUnit_Framework_TestCase', true)) { + class_alias('\PHPUnit\Framework\TestCase', '\PHPUnit_Framework_TestCase', true); +} + +/** + * Test for the core:Cardinality filter. + */ +class Test_Core_Auth_Process_CardinalityTest extends \PHPUnit_Framework_TestCase +{ + /** + * Helper function to run the filter with a given configuration. + * + * @param array $config The filter configuration. + * @param array $request The request state. + * @return array The state array after processing. + */ + private static function processFilter(array $config, array $request) + { + $_SERVER['SERVER_PROTOCOL'] = 'HTTP/1.1'; + $_SERVER['REQUEST_METHOD'] = 'GET'; + $filter = new sspmod_core_Auth_Process_Cardinality($config, null); + $filter->process($request); + return $request; + } + + protected function setUp() + { + \SimpleSAML_Configuration::loadFromArray(array(), '[ARRAY]', 'simplesaml'); + } + + /* + * Test where a minimum is set but no maximum + */ + public function testMinNoMax() + { + $config = array( + 'mail' => array('min' => 1), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('mail' => array('joe@example.com', 'bob@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /* + * Test where a maximum is set but no minimum + */ + public function testMaxNoMin() + { + $config = array( + 'mail' => array('max' => 2), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('mail' => array('joe@example.com', 'bob@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /* + * Test in bounds within a maximum an minimum + */ + public function testMaxMin() + { + $config = array( + 'mail' => array('min' => 1, 'max' => 2), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + $attributes = $result['Attributes']; + $expectedData = array('mail' => array('joe@example.com', 'bob@example.com')); + $this->assertEquals($expectedData, $attributes, "Assertion values should not have changed"); + } + + /** + * Test maximum is out of bounds results in redirect + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessageRegExp /REQUEST_URI/ + */ + public function testMaxOutOfBounds() + { + $config = array( + 'mail' => array('max' => 2), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com', 'fred@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test minimum is out of bounds results in redirect + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessageRegExp /REQUEST_URI/ + */ + public function testMinOutOfBounds() + { + $config = array( + 'mail' => array('min' => 3), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test missing attribute results in redirect + * @expectedException PHPUnit_Framework_Error + * @expectedExceptionMessageRegExp /REQUEST_URI/ + */ + public function testMissingAttribute() + { + $config = array( + 'mail' => array('min' => 1), + ); + $request = array( + 'Attributes' => array( ), + ); + $result = self::processFilter($config, $request); + } + + /* + * Configuration errors + */ + + /** + * Test invalid minimum values + * @expectedException SimpleSAML_Error_Exception + * @expectedExceptionMessageRegExp /Minimum/ + */ + public function testMinInvalid() + { + $config = array( + 'mail' => array('min' => false), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test invalid minimum values + * @expectedException SimpleSAML_Error_Exception + * @expectedExceptionMessageRegExp /Minimum/ + */ + public function testMinNegative() + { + $config = array( + 'mail' => array('min' => -1), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test invalid maximum values + * @expectedException SimpleSAML_Error_Exception + * @expectedExceptionMessageRegExp /Maximum/ + */ + public function testMaxInvalid() + { + $config = array( + 'mail' => array('max' => false), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test maximum < minimum + * @expectedException SimpleSAML_Error_Exception + * @expectedExceptionMessageRegExp /less than/ + */ + public function testMinGreaterThanMax() + { + $config = array( + 'mail' => array('min' => 2, 'max' => 1), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } + + /** + * Test invalid attribute name + * @expectedException SimpleSAML_Error_Exception + * @expectedExceptionMessageRegExp /Invalid attribute/ + */ + public function testInvalidAttributeName() + { + $config = array( + array('min' => 2, 'max' => 1), + ); + $request = array( + 'Attributes' => array( + 'mail' => array('joe@example.com', 'bob@example.com'), + ), + ); + $result = self::processFilter($config, $request); + } +} -- GitLab