diff --git a/modules/core/dictionaries/cardinality.definition.json b/modules/core/dictionaries/cardinality.definition.json new file mode 100644 index 0000000000000000000000000000000000000000..b9059cb93a389ad4422be193ae8b17371ed426cf --- /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 0000000000000000000000000000000000000000..d54ab371fbefc1cec8a11e65efae46e0e1996980 --- /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 0000000000000000000000000000000000000000..0902bf27863dad94991aacb54d15a7a407750f63 --- /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 0000000000000000000000000000000000000000..4a61bdbc5aaca4579729fc0bafec6b6976a2efea --- /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 0000000000000000000000000000000000000000..74d330f00c7f1f7526eb2ad45ffa61df1171e223 --- /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 0000000000000000000000000000000000000000..459d57d8ba162893bcd20fa4f5e1585d43ac5731 --- /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 0000000000000000000000000000000000000000..dc75c540afc9b7f71a167a5d397188eca3cb3e47 --- /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 0000000000000000000000000000000000000000..4b36495fb859baa797ce67b11eb3660aca336b25 --- /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 0000000000000000000000000000000000000000..ccc03bf7c7bbabc640afe0febaf30af2c5a88888 --- /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 0000000000000000000000000000000000000000..c9df640c60f4aecdbb75d720369bee74f4459fa7 --- /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); + } +}