diff --git a/modules/aggregator2/lib/Aggregator.php b/modules/aggregator2/lib/Aggregator.php index 61ec7b003a34c9c3d71eccfbb2d213738fea1329..6d4db6acf287a9539ed5bbac7ec629aa7533e5b0 100644 --- a/modules/aggregator2/lib/Aggregator.php +++ b/modules/aggregator2/lib/Aggregator.php @@ -67,6 +67,41 @@ class sspmod_aggregator2_Aggregator { protected $cacheGenerated; + /** + * An array of entity IDs to exclude from the aggregate. + * + * @var string[]|null + */ + protected $excluded; + + + /** + * An indexed array of protocols to filter the aggregate by. keys can be any of: + * + * - urn:oasis:names:tc:SAML:1.1:protocol + * - urn:oasis:names:tc:SAML:2.0:protocol + * + * Values will be true if enabled, false otherwise. + * + * @var string[]|null + */ + protected $protocols; + + + /** + * An array of roles to filter the aggregate by. Keys can be any of: + * + * - SAML2_XML_md_IDPSSODescriptor + * - SAML2_XML_md_SPSSODescriptor + * - SAML2_XML_md_AttributeAuthorityDescriptor + * + * Values will be true if enabled, false otherwise. + * + * @var string[]|null + */ + protected $roles; + + /** * The key we should use to sign the metadata. * @@ -151,6 +186,12 @@ class sspmod_aggregator2_Aggregator { $this->cacheTag = sha1(serialize($config)); } + // configure entity IDs excluded by default + $this->excludeEntities($config->getArrayize('exclude', null)); + + // configure filters + $this->setFilters($config->getArrayize('filter', null)); + $this->validLength = $config->getInteger('valid.length', 7*24*60*60); $globalConfig = SimpleSAML_Configuration::getInstance(); @@ -424,6 +465,159 @@ class sspmod_aggregator2_Aggregator { } + /** + * Recursively traverse the children of an EntitiesDescriptor, removing those entities listed in the $entities + * property. Returns the EntitiesDescriptor with the entities filtered out. + * + * @param SAML2_XML_md_EntitiesDescriptor $descriptor The EntitiesDescriptor from where to exclude entities. + * + * @return SAML2_XML_md_EntitiesDescriptor The EntitiesDescriptor with excluded entities filtered out. + */ + protected function exclude(SAML2_XML_md_EntitiesDescriptor $descriptor) + { + if (empty($this->excluded)) { + return $descriptor; + } + + $filtered = array(); + foreach ($descriptor->children as $child) { + if ($child instanceof SAML2_XML_md_EntityDescriptor) { + if (in_array($child->entityID, $this->excluded)) { + continue; + } + $filtered[] = $child; + } + + if ($child instanceof SAML2_XML_md_EntitiesDescriptor) { + $filtered[] = $this->exclude($child); + } + } + + $descriptor->children = $filtered; + return $descriptor; + } + + + /** + * Recursively traverse the children of an EntitiesDescriptor, keeping only those entities with the roles listed in + * the $roles property, and support for the protocols listed in the $protocols property. Returns the + * EntitiesDescriptor containing only those entities. + * + * @param SAML2_XML_md_EntitiesDescriptor $descriptor The EntitiesDescriptor to filter. + * + * @return SAML2_XML_md_EntitiesDescriptor The EntitiesDescriptor with only the entities filtered. + */ + protected function filter(SAML2_XML_md_EntitiesDescriptor $descriptor) + { + if ($this->roles === null || $this->protocols === null) { + return $descriptor; + } + + $enabled_roles = array_keys($this->roles, true); + $enabled_protos = array_keys($this->protocols, true); + + $filtered = array(); + foreach ($descriptor->children as $child) { + if ($child instanceof SAML2_XML_md_EntityDescriptor) { + foreach ($child->RoleDescriptor as $role) { + if (in_array(get_class($role), $enabled_roles)) { + // we found a role descriptor that is enabled by our filters, check protocols + if (array_intersect($enabled_protos, $role->protocolSupportEnumeration) !== array()) { + // it supports some protocol we have enabled, add it + $filtered[] = $child; + break; + } + } + } + + } + + if ($child instanceof SAML2_XML_md_EntitiesDescriptor) { + $filtered[] = $this->filter($child); + } + } + + $descriptor->children = $filtered; + return $descriptor; + } + + + /** + * Set this aggregator to exclude a set of entities from the resulting aggregate. + * + * @param array|null $entities The entity IDs of the entities to exclude. + */ + public function excludeEntities($entities) + { + assert('is_array($entities) || is_null($entities)'); + + if ($entities === null) { + return; + } + $this->excluded = $entities; + sort($this->excluded); + $this->cacheId = sha1($this->cacheId . serialize($this->excluded)); + } + + + /** + * Set the internal filters according to one or more options: + * + * - 'saml2': all SAML2.0-capable entities. + * - 'shib13': all SHIB1.3-capable entities. + * - 'saml20-idp': all SAML2.0-capable identity providers. + * - 'saml20-sp': all SAML2.0-capable service providers. + * - 'saml20-aa': all SAML2.0-capable attribute authorities. + * - 'shib13-idp': all SHIB1.3-capable identity providers. + * - 'shib13-sp': all SHIB1.3-capable service providers. + * - 'shib13-aa': all SHIB1.3-capable attribute authorities. + * + * @param array|null $set An array of the different roles and protocols to filter by. + */ + public function setFilters($set) + { + assert('is_array($set) || is_null($set)'); + + if ($set === null) { + return; + } + + // configure filters + $this->protocols = array( + SAML2_Const::NS_SAMLP => TRUE, + 'urn:oasis:names:tc:SAML:1.1:protocol' => TRUE, + ); + $this->roles = array( + 'SAML2_XML_md_IDPSSODescriptor' => TRUE, + 'SAML2_XML_md_SPSSODescriptor' => TRUE, + 'SAML2_XML_md_AttributeAuthorityDescriptor' => TRUE, + ); + + // now translate from the options we have, to specific protocols and roles + + // check SAML 2.0 protocol + $options = array('saml2', 'saml20-idp', 'saml20-sp', 'saml20-aa'); + $this->protocols[SAML2_Const::NS_SAMLP] = (array_intersect($set, $options) !== array()); + + // check SHIB 1.3 protocol + $options = array('shib13', 'shib13-idp', 'shib13-sp', 'shib13-aa'); + $this->protocols['urn:oasis:names:tc:SAML:1.1:protocol'] = (array_intersect($set, $options) !== array()); + + // check IdP + $options = array('saml2', 'shib13', 'saml20-idp', 'shib13-idp'); + $this->roles['SAML2_XML_md_IDPSSODescriptor'] = (array_intersect($set, $options) !== array()); + + // check SP + $options = array('saml2', 'shib13', 'saml20-sp', 'shib13-sp'); + $this->roles['SAML2_XML_md_SPSSODescriptor'] = (array_intersect($set, $options) !== array()); + + // check AA + $options = array('saml2', 'shib13', 'saml20-aa', 'shib13-aa'); + $this->roles['SAML2_XML_md_AttributeAuthorityDescriptor'] = (array_intersect($set, $options) !== array()); + + $this->cacheId = sha1($this->cacheId . serialize($this->protocols) . serialize($this->roles)); + } + /** * Retrieve the complete, signed metadata as text. * @@ -435,6 +629,8 @@ class sspmod_aggregator2_Aggregator { public function updateCachedMetadata() { $ed = $this->getEntitiesDescriptor(); + $ed = $this->exclude($ed); + $ed = $this->filter($ed); $this->addSignature($ed); $xml = $ed->toXML(); diff --git a/modules/aggregator2/www/get.php b/modules/aggregator2/www/get.php index 8ace0a5061301ed683cd0df25ef070d85f2c8a4d..1a07714b65cd3523f12a50032d8fd8db127c8a03 100644 --- a/modules/aggregator2/www/get.php +++ b/modules/aggregator2/www/get.php @@ -5,7 +5,19 @@ if (!isset($_REQUEST['id'])) { } $id = (string) $_REQUEST['id']; +$set = null; +if (isset($_REQUEST['set'])) { + $set = explode(',', $_REQUEST['set']); +} + +$excluded_entities = null; +if (isset($_REQUEST['exclude'])) { + $excluded_entities = explode(',', $_REQUEST['exclude']); +} + $aggregator = sspmod_aggregator2_Aggregator::getAggregator($id); +$aggregator->setFilters($set); +$aggregator->excludeEntities($excluded_entities); $xml = $aggregator->getMetadata(); $mimetype = 'application/samlmetadata+xml';