diff --git a/modules/aggregator/config-templates/aggregator.php b/modules/aggregator/config-templates/module_aggregator.php similarity index 61% rename from modules/aggregator/config-templates/aggregator.php rename to modules/aggregator/config-templates/module_aggregator.php index e17552425fc9468846744effa3fb224e72f3adbc..1c2d851c00a949a14b2acdaf86e042ee216f2673 100644 --- a/modules/aggregator/config-templates/aggregator.php +++ b/modules/aggregator/config-templates/module_aggregator.php @@ -6,19 +6,28 @@ $config = array( /* List of aggregators. */ 'aggregators' => array( 'example' => array( - array('type' => 'flatfile'), /* Metadata from metadata-directory. */ - array('type' => 'xml', 'url' => 'https://idp.example.org/Metadata'), - array('type' => 'xml', 'file' => 'static-metadata.xml'), + 'source' => array( + array('type' => 'flatfile'), /* Metadata from metadata-directory. */ + array('type' => 'xml', 'url' => 'https://idp.example.org/Metadata'), + array('type' => 'xml', 'file' => 'static-metadata.xml'), + ), ), + 'example2' => array( + 'source' => array( + array('type' => 'xml', 'url' => 'https://idp.example.org/Metadata2'), + ), + 'set' => 'saml2', + 'sign.privatekey' => 'server2.key', + 'sign.certificate' => 'server2.crt', + ) ), - 'maxCache' => 60*60*24, // 24 hour cache time 'maxDuration' => 60*60*24*5, // Maximum 5 days duration on ValidUntil. // If base64 encoded for entity is already cached in the entity, should we // reconstruct the XML or re-use. - 'reconstruct' => TRUE, + 'reconstruct' => FALSE, /* Whether metadata should be signed. */ 'sign.enable' => FALSE, @@ -34,4 +43,3 @@ $config = array( ); -?> \ No newline at end of file diff --git a/modules/aggregator/docs/aggregator.txt b/modules/aggregator/docs/aggregator.txt new file mode 100644 index 0000000000000000000000000000000000000000..14f5042a30853b0768323f36b5a8179f44670e37 --- /dev/null +++ b/modules/aggregator/docs/aggregator.txt @@ -0,0 +1,90 @@ +aggregator Module +================= + +<!-- + This file is written in Markdown syntax. + For more information about how to use the Markdown syntax, read here: + http://daringfireball.net/projects/markdown/syntax +--> + + * Version: `$Id$` + * Author: Andreas Ă…kre Solberg <andreas.solberg@uninett.no>, UNINETT AS + * Package: simpleSAMLphp + +This module, aggregates a set of metadata of SAML entities to SAML 2.0 documents with an `EntitiesDescriptor` with multiple entities inside. + +Multiple aggregates can be configured. + + + +The configuration file: module_aggregate.php +-------------------------------------------- + +The configuration file includes an option `aggregators`, which includes a indexed list of different aggregator configurations that all can be accessed independently. The structure is as follows: + + 'aggregators' => array( + 'aggr1' => array( + 'sources' => [...] + [...local params...] + ), + 'aggr2' => ... + ) + [...global params...] + +All of the global parameters can be overriden for each aggregator. Here is a list of the available (global) paramters: + +`set` +: By default all SAML types are available, including: `array('saml20-idp-remote', 'saml20-sp-remote', 'shib13-idp-remote', 'shib13-sp-remote')`. This list can be reduced by specifying one of the following values: + + * `saml20-idp-remote` + * `saml20-sp-remote` + * `shib13-idp-remote` + * `shib13-sp-remote` + * `saml2` + * `shib13` + +`foo` +: sldkfjdslkjf + +`reconstruct` +: Whether simpleSAMLphp should regenerate the metadata XML (TRUE) or pass-through the input metadata XML (FALSE). + +`maxDuration` +: Max validity of metadata (duration) in seconds. + +`sign.enable` +: Enable signing of metadata document + +`sign.privatekey` +: Private key to use when signing + +`sign.privatekey_pass` +: Optionally a passphrase to the private key + +`sign.certificate` +: Certificate to embed, corresponding to the private key. + + +Accessing the aggregate +----------------------- + +On the SimpleSAMLphp frontpage on the federation tab, there is a link to the aggregator named *Metadata aggregator*. + +When accessing the aggregator endpoint without specifying an aggregate ID, a list of available aggregators will be presented, with different options for mime-type presenting the result. + +The endpoint supports the following query parameter: + +`id` +: The ID of the aggregator (From configuration file) + +`set` +: Subset the available types of SAML entities. Similar to the `set` parameter described over in the configuration file description. + +`exclude` +: Specify a `tag` that will be excluded from the metadata set. Useful for leaving out your own federation metadata. + +`mimetype` +: Select Mime-Type that will be used. Default is `application/samlmetadata+xml`. + + + diff --git a/modules/aggregator/lib/Aggregator.php b/modules/aggregator/lib/Aggregator.php new file mode 100644 index 0000000000000000000000000000000000000000..1f3474b58dfd959afc4556f30e30bf62f9560d7c --- /dev/null +++ b/modules/aggregator/lib/Aggregator.php @@ -0,0 +1,247 @@ +<?php + +/** + * Aggregates metadata for multiple sources into one signed file + * + * @author Andreas Ă…kre Solberg <andreas.solberg@uninett.no> + * @package simpleSAMLphp + * @version $Id$ + */ + +class sspmod_aggregator_Aggregator { + + // Configuration for the whole aggregator module + private $gConfig; + + // Configuration for the specific aggregate + private $aConfig; + + private $sets; + + private $excludeTags = array(); + + private $id; + + /** + * Constructor for the Aggregator. + * + */ + public function __construct($gConfig, $aConfig, $id) { + $this->gConfig = $gConfig; + $this->aConfig = $aConfig; + $this->id = $id; + + $this->sets = array('saml20-idp-remote', 'saml20-sp-remote', 'shib13-idp-remote', 'shib13-sp-remote'); + + if ($this->aConfig->hasValue('set')) { + $this->limitSets($this->aConfig->getString('set')); + } + } + + + + public function limitSets($set) { + + + if (is_array($set)) { + $this->sets = array_intersect($this->sets, $set); + return; + } + + switch($set) { + case 'saml2' : + $this->sets = array_intersect($this->sets, array('saml20-idp-remote', 'saml20-sp-remote')); break; + case 'shib13' : + $this->sets = array_intersect($this->sets, array('shib13-idp-remote', 'shib13-sp-remote')); break; + case 'idp' : + $this->sets = array_intersect($this->sets, array('saml20-idp-remote', 'shib13-idp-remote')); break; + case 'sp' : + $this->sets = array_intersect($this->sets, array('saml20-sp-remote', 'shib13-sp-remote')); break; + + default: + $this->sets = array_intersect($this->sets, array($set)); + } + } + + /** + * Add tag to excelude when collecting source metadata. + * + * $exclude May be string or array identifying a tag to exclude. + */ + public function exclude($exclude) { + $this->excludeTags = array_merge($this->excludeTags, SimpleSAML_Utilities::arrayize($exclude)); + } + + /** + * Returns a list of entities with metadata + */ + public function getSources() { + + $sourcesDef = $this->aConfig->getArray('sources'); + + try { + $sources = SimpleSAML_Metadata_MetaDataStorageSource::parseSources($sourcesDef); + } catch (Exception $e) { + throw new Exception('Invalid aggregator source configuration for aggregator ' . + var_export($id, TRUE) . ': ' . $e->getMessage()); + } + + + #echo $exclude; exit; + /* Find list of all available entities. */ + $entities = array(); + #echo '<pre>'; print_r($this->sets); exit; + + foreach ($sources as $source) { + foreach ($this->sets as $set) { + + foreach ($source->getMetadataSet($set) as $entityId => $metadata) { + + if (isset($metadata['tags']) && + count(array_intersect($this->excludeTags, $metadata['tags'])) > 0) { + + SimpleSAML_Logger::debug('Excluding entity ID [' . $entityId . '] becuase it is tagged with one of [' . + var_export($this->excludeTags, TRUE) . ']'); + continue; + } else { + #echo('<pre>'); print_r($metadata); exit; + } + if (!array_key_exists($entityId, $entities)) + $entities[$entityId] = array(); + + if (array_key_exists($set, $entities[$entityId])) { + /* Entity already has metadata for the given set. */ + continue; + } + + $entities[$entityId][$set] = $metadata; + } + } + } + return $entities; + + } + + public function getMaxDuration() { + if ($this->aConfig->hasValue('maxDuration')) + return $this->aConfig->getInteger('maxDuration'); + if ($this->gConfig->hasValue('maxDuration')) + return $this->gConfig->getInteger('maxDuration'); + return NULL; + } + + + + public function getReconstruct() { + if ($this->aConfig->hasValue('reconstruct')) + return $this->aConfig->getBoolean('reconstruct'); + if ($this->gConfig->hasValue('reconstruct')) + return $this->gConfig->getBoolean('reconstruct'); + return FALSE; + } + + public function shouldSign() { + if ($this->aConfig->hasValue('sign.enable')) + return $this->aConfig->getBoolean('sign.enable'); + if ($this->gConfig->hasValue('sign.enable')) + return $this->gConfig->getBoolean('sign.enable'); + return FALSE; + } + + + public function getSigningInfo() { + if ($this->aConfig->hasValue('sign.privatekey')) { + return array( + 'privatekey' => $this->aConfig->getString('sign.privatekey'), + 'privatekey_pass' => $this->aConfig->getString('sign.privatekey_pass', NULL), + 'certificate' => $this->aConfig->getString('sign.certificate'), + 'id' => 'ID' + ); + } + + return array( + 'privatekey' => $this->gConfig->getString('sign.privatekey'), + 'privatekey_pass' => $this->gConfig->getString('sign.privatekey_pass', NULL), + 'certificate' => $this->gConfig->getString('sign.certificate'), + 'id' => 'ID' + ); + } + + + + public function getMetadataDocument() { + + // Get metadata entries + $entities = $this->getSources(); + + + // Generate XML Document + $xml = new DOMDocument(); + $entitiesDescriptor = $xml->createElementNS('urn:oasis:names:tc:SAML:2.0:metadata', 'EntitiesDescriptor'); + $entitiesDescriptor->setAttribute('Name', $this->id); + $xml->appendChild($entitiesDescriptor); + + + $maxDuration = $this->getMaxDuration(); + $reconstruct = $this->getReconstruct(); + + + /* Build EntityDescriptor elements for them. */ + foreach ($entities as $entity => $sets) { + + $entityDescriptor = NULL; + foreach ($sets as $set => $metadata) { + if (!array_key_exists('entityDescriptor', $metadata)) { + /* One of the sets doesn't contain an EntityDescriptor element. */ + $entityDescriptor = FALSE; + break; + } + + if ($entityDescriptor == NULL) { + /* First EntityDescriptor elements. */ + $entityDescriptor = $metadata['entityDescriptor']; + continue; + } + + assert('is_string($entityDescriptor)'); + if ($entityDescriptor !== $metadata['entityDescriptor']) { + /* Entity contains multiple different EntityDescriptor elements. */ + $entityDescriptor = FALSE; + break; + } + } + + if (is_string($entityDescriptor) && !$reconstruct) { + /* All metadata sets for the entity contain the same entity descriptor. Use that one. */ + $tmp = new DOMDocument(); + $tmp->loadXML(base64_decode($entityDescriptor)); + $entityDescriptor = $tmp->documentElement; + } else { + + $tmp = new SimpleSAML_Metadata_SAMLBuilder($entity, $maxDuration, $maxDuration); + + $orgmeta = NULL; + foreach ($sets as $set => $metadata) { + $tmp->addMetadata($set, $metadata); + $orgmeta = $metadata; + } + $tmp->addOrganizationInfo($orgmeta); + $entityDescriptor = $tmp->getEntityDescriptor(); + } + + $entitiesDescriptor->appendChild($xml->importNode($entityDescriptor, TRUE)); + } + + + /* Sign the metadata if enabled. */ + if ($this->shouldSign()) { + $signer = new SimpleSAML_XML_Signer($this->getSigningInfo()); + $signer->sign($entitiesDescriptor, $entitiesDescriptor, $entitiesDescriptor->firstChild); + } + + + return $xml; + } + + +} diff --git a/modules/aggregator/www/index.php b/modules/aggregator/www/index.php index 36a34dc14812f3fab0dde0b1ef4ff95f2b7c9508..618abfb8f8497ba54754c5d92831578524cc8187 100644 --- a/modules/aggregator/www/index.php +++ b/modules/aggregator/www/index.php @@ -1,159 +1,36 @@ <?php -$globalConfig = SimpleSAML_Configuration::getInstance(); -$aggregatorConfig = SimpleSAML_Configuration::getConfig('aggregator.php'); +$config = SimpleSAML_Configuration::getInstance(); +$gConfig = SimpleSAML_Configuration::getConfig('module_aggregator.php'); -$reconstruct = $aggregatorConfig->getBoolean('reconstruct', FALSE); -$aggregators = $aggregatorConfig->getArray('aggregators'); - -$metadataSets = array('saml20-idp-remote', 'saml20-sp-remote', 'shib13-idp-remote', 'shib13-sp-remote'); -if ($aggregatorConfig->hasValue('default-set')) { - $metadataSets = $aggregatorConfig->getArray('default-set'); -} -if (isset($_REQUEST['set'])) { - switch($_REQUEST['set']) { - case 'saml2' : - $metadataSets = array('saml20-idp-remote', 'saml20-sp-remote'); break; - case 'shib13' : - $metadataSets = array('shib13-idp-remote', 'shib13-sp-remote'); break; - case 'idp' : - $metadataSets = array('saml20-idp-remote', 'shib13-idp-remote'); break; - case 'sp' : - $metadataSets = array('saml20-sp-remote', 'shib13-sp-remote'); break; - - default: - $metadataSets = array($_REQUEST['set']); - } -} - -// print_r($metadataSets); exit; +// Get list of aggregators +$aggregators = $gConfig->getConfigItem('aggregators'); +// If aggregator ID is not provided, show the list of available aggregates if (!array_key_exists('id', $_GET)) { - $t = new SimpleSAML_XHTML_Template($globalConfig, 'aggregator:list.php'); - $t->data['sources'] = array_keys($aggregators); + $t = new SimpleSAML_XHTML_Template($config, 'aggregator:list.php'); + $t->data['sources'] = $aggregators->getOptions(); $t->show(); exit; } - $id = $_GET['id']; -if (!array_key_exists($id, $aggregators)) { +if (!in_array($id, $aggregators->getOptions())) throw new SimpleSAML_Error_NotFound('No aggregator with id ' . var_export($id, TRUE) . ' found.'); -} +$aConfig = $aggregators->getConfigItem($id); -/* Parse metadata sources. */ -$sources = $aggregators[$id]; -if (!is_array($sources)) { - throw new Exception('Invalid aggregator source configuration for aggregator ' . - var_export($id, TRUE) . ': Aggregator wasn\'t an array.'); -}; -try { - $sources = SimpleSAML_Metadata_MetaDataStorageSource::parseSources($sources); -} catch (Exception $e) { - throw new Exception('Invalid aggregator source configuration for aggregator ' . - var_export($id, TRUE) . ': ' . $e->getMessage()); -} +$aggregator = new sspmod_aggregator_Aggregator($gConfig, $aConfig, $id); -$exclude = NULL; -if (array_key_exists('exclude', $_REQUEST)) $exclude = $_REQUEST['exclude']; - -#echo $exclude; exit; -/* Find list of all available entities. */ -$entities = array(); -foreach ($sources as $source) { - foreach ($metadataSets as $set) { - foreach ($source->getMetadataSet($set) as $entityId => $metadata) { - if (isset($exclude) && - array_key_exists('tags', $metadata) && - in_array($exclude, $metadata['tags'])) { - SimpleSAML_Logger::debug('Excluding entity ID [' . $entityId . '] becuase it is tagged with [' . $exclude . ']'); - continue; - } else { - #echo('<pre>'); print_r($metadata); exit; - } - if (!array_key_exists($entityId, $entities)) - $entities[$entityId] = array(); - - if (array_key_exists($set, $entities[$entityId])) { - /* Entity already has metadata for the given set. */ - continue; - } - - $entities[$entityId][$set] = $metadata; - } - } -} +if (isset($_REQUEST['set'])) + $aggregator->limitSets($_REQUEST['set']); -$xml = new DOMDocument(); -$entitiesDescriptor = $xml->createElementNS('urn:oasis:names:tc:SAML:2.0:metadata', 'EntitiesDescriptor'); -$entitiesDescriptor->setAttribute('Name', $id); -$xml->appendChild($entitiesDescriptor); - - - -/* Build EntityDescriptor elements for them. */ -foreach ($entities as $entity => $sets) { - - $entityDescriptor = NULL; - foreach ($sets as $set => $metadata) { - if (!array_key_exists('entityDescriptor', $metadata)) { - /* One of the sets doesn't contain an EntityDescriptor element. */ - $entityDescriptor = FALSE; - break; - } - - if ($entityDescriptor == NULL) { - /* First EntityDescriptor elements. */ - $entityDescriptor = $metadata['entityDescriptor']; - continue; - } - - assert('is_string($entityDescriptor)'); - if ($entityDescriptor !== $metadata['entityDescriptor']) { - /* Entity contains multiple different EntityDescriptor elements. */ - $entityDescriptor = FALSE; - break; - } - } - - if (is_string($entityDescriptor) && !$reconstruct) { - /* All metadata sets for the entity contain the same entity descriptor. Use that one. */ - $tmp = new DOMDocument(); - $tmp->loadXML(base64_decode($entityDescriptor)); - $entityDescriptor = $tmp->documentElement; - } else { - $tmp = new SimpleSAML_Metadata_SAMLBuilder($entity, - $aggregatorConfig->getValue('maxCache', NULL), $aggregatorConfig->getValue('maxDuration', NULL)); - - $orgmeta = NULL; - foreach ($sets as $set => $metadata) { - $tmp->addMetadata($set, $metadata); - $orgmeta = $metadata; - } - $tmp->addOrganizationInfo($orgmeta); - $entityDescriptor = $tmp->getEntityDescriptor(); - } - - $entitiesDescriptor->appendChild($xml->importNode($entityDescriptor, TRUE)); -} +if (isset($_REQUEST['exclude'])) + $aggregator->exclude($_REQUEST['exclude']); -/* Sign the metadata if enabled. */ -if ($aggregatorConfig->getBoolean('sign.enable', FALSE)) { - $privateKey = $aggregatorConfig->getString('sign.privatekey'); - $privateKeyPass = $aggregatorConfig->getString('sign.privatekey_pass', NULL); - $certificate = $aggregatorConfig->getString('sign.certificate'); - - $signer = new SimpleSAML_XML_Signer(array( - 'privatekey' => $privateKey, - 'privatekey_pass' => $privateKeyPass, - 'certificate' => $certificate, - 'id' => 'ID', - )); - $signer->sign($entitiesDescriptor, $entitiesDescriptor, $entitiesDescriptor->firstChild); -} +$xml = $aggregator->getMetadataDocument(); /* Show the metadata. */