diff --git a/docs/simplesamlphp-maintenance.md b/docs/simplesamlphp-maintenance.md index 1d99f19f77..ca5b04e671 100644 --- a/docs/simplesamlphp-maintenance.md +++ b/docs/simplesamlphp-maintenance.md @@ -165,13 +165,14 @@ By default SimpleSAMLphp will attempt to connect to Redis on the `localhost` at ## Metadata storage -Several metadata storage backends are available by default, including `flatfile`, `serialize`, `mdq` and +Several metadata storage backends are available by default, including `flatfile`, `directory`, `serialize`, `mdq` and [`pdo`](https://simplesamlphp.org/docs/stable/simplesamlphp-metadata-pdostoragehandler). Here you have an example configuration of different metadata sources in use at the same time: ``` 'metadata.sources' => [ ['type' => 'flatfile'], + ['type' => 'directory'], ['type' => 'flatfile', 'directory' => 'metadata/metarefresh-kalmar'], ['type' => 'serialize', 'directory' => 'metadata/metarefresh-ukaccess'], ], diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageHandlerDirectoryOfFiles.php b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerDirectoryOfFiles.php new file mode 100644 index 0000000000..0c355b7577 --- /dev/null +++ b/lib/SimpleSAML/Metadata/MetaDataStorageHandlerDirectoryOfFiles.php @@ -0,0 +1,162 @@ + + * @package SimpleSAMLphp + * + * Based on MetaDataStorageHandlerFlatFile by Andreas Åkre Solberg, UNINETT AS. and + * MetaDataStorageHandlerSerialize.php + */ + +class MetaDataStorageHandlerDirectoryOfFiles extends MetaDataStorageSource +{ + /** + * This is the directory we will load metadata files from. The path will always end + * with a '/'. + * + * @var string + */ + private $directory = '/'; + + + /** + * This is an associative array which stores the different metadata sets we have loaded. + * + * @var array + */ + private $cachedMetadata = []; + + /** + * The extension we use for our metadata directories. + * + * @var string + */ + const EXTENSION = '.d'; + + + /** + * This constructor initializes the directory of files metadata storage handler with the + * specified configuration. The configuration is an associative array with the following + * possible elements: + * - 'directory': The directory we should load metadata from. The default directory is + * set in the 'metadatadir' configuration option in 'config.php'. + * + * @param array $config An associative array with the configuration for this handler. + */ + protected function __construct(array $config) + { + // get the configuration + $globalConfig = Configuration::getInstance(); + + // find the path to the directory we should search for metadata in + if (array_key_exists('directory', $config)) { + $this->directory = $config['directory'] ?: 'metadata/'; + } else { + $this->directory = $globalConfig->getString('metadatadir', 'metadata/'); + } + + /* Resolve this directory relative to the SimpleSAMLphp directory (unless it is + * an absolute path). + */ + + /** @var string $base */ + $base = $globalConfig->resolvePath($this->directory); + $this->directory = $base . '/'; + } + + + /** + * This function loads the given set of metadata from a file our metadata directory. + * This function returns null if it is unable to locate the given set in the metadata directory. + * + * @param string $set The set of metadata we are loading. + * + * @return array|null An associative array with the metadata, + * or null if we are unable to load metadata from the given file. + * @throws \Exception If the metadata set cannot be loaded. + */ + private function load(string $set): ?array + { + $metadatasetdir = $this->directory . $set . self::EXTENSION; + + /** @psalm-var mixed $metadata We cannot be sure what the include below will do with this var */ + $metadata = []; + + $dh = @opendir($metadatasetdir); + if ($dh === false) { + Logger::warning( + 'Directory metadata handler: Unable to open directory: ' . var_export($metadatasetdir, true) + ); + return $metadata; + } + + while (($entry = readdir($dh)) !== false) { + if ($entry[0] === '.') { + // skip '..', '.' and hidden files + continue; + } + + $path = $metadatasetdir.DIRECTORY_SEPARATOR . $entry; + + if (is_dir($path)) { + Logger::warning( + 'Directory metadata handler: Metadata directory contained a directory where only files should ' . + 'exist: ' . var_export($path, true) + ); + continue; + } + + include($path); + } + + closedir($dh); + + if (!is_array($metadata)) { + throw new \Exception('Could not load metadata set [' . $set . '] from directory: ' . $metadatasetdir); + } + + return $metadata; + } + + + /** + * This function retrieves the given set of metadata. It will return an empty array if it is + * unable to locate it. + * + * @param string $set The set of metadata we are retrieving. + * + * @return array An associative array with the metadata. Each element in the array is an entity, and the + * key is the entity id. + */ + public function getMetadataSet(string $set): array + { + if (array_key_exists($set, $this->cachedMetadata)) { + return $this->cachedMetadata[$set]; + } + + $metadataSet = $this->load($set); + if ($metadataSet === null) { + $metadataSet = []; + } + + // add the entity id of an entry to each entry in the metadata + foreach ($metadataSet as $entityId => &$entry) { + $entry = $this->updateEntityID($set, $entityId, $entry); + } + + $this->cachedMetadata[$set] = $metadataSet; + return $metadataSet; + } +} diff --git a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php index 875a488ccc..4ccdc06572 100644 --- a/lib/SimpleSAML/Metadata/MetaDataStorageSource.php +++ b/lib/SimpleSAML/Metadata/MetaDataStorageSource.php @@ -77,6 +77,8 @@ public static function getSource(array $sourceConfig): MetaDataStorageSource return new MetaDataStorageHandlerXML($sourceConfig); case 'serialize': return new MetaDataStorageHandlerSerialize($sourceConfig); + case 'directory': + return new MetaDataStorageHandlerDirectoryOfFiles($sourceConfig); case 'mdx': case 'mdq': return new Sources\MDQ($sourceConfig); diff --git a/tests/lib/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php b/tests/lib/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php index 47e837ce4c..1ab0eb5310 100644 --- a/tests/lib/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php +++ b/tests/lib/SimpleSAML/Metadata/MetaDataStorageHandlerTest.php @@ -20,6 +20,7 @@ public function testLoadEntities() 'metadata.sources' => [ ['type' => 'flatfile', 'directory' => __DIR__ . '/test-metadata/source1'], ['type' => 'serialize', 'directory' => __DIR__ . '/test-metadata/source2'], + ['type' => 'directory', 'directory' => __DIR__ . '/test-metadata/source3'], ], ]; Configuration::loadFromArray($c, '', 'simplesaml'); @@ -28,13 +29,15 @@ public function testLoadEntities() $entities = $handler->getMetaDataForEntities([ 'entityA', 'entityB', + 'entityC', 'nosuchEntity', 'entityInBoth', 'expiredInSrc1InSrc2' ], 'saml20-sp-remote'); - $this->assertCount(4, $entities); + $this->assertCount(5, $entities); $this->assertEquals('entityA SP from source1', $entities['entityA']['name']['en']); $this->assertEquals('entityB SP from source2', $entities['entityB']['name']['en']); + $this->assertEquals('entityC SP from source3', $entities['entityC']['name']['en']); $this->assertEquals( 'entityInBoth SP from source1', $entities['entityInBoth']['name']['en'], diff --git a/tests/lib/SimpleSAML/Metadata/test-metadata/source3/saml20-sp-remote.d/entityC.php b/tests/lib/SimpleSAML/Metadata/test-metadata/source3/saml20-sp-remote.d/entityC.php new file mode 100644 index 0000000000..aa94646ca9 --- /dev/null +++ b/tests/lib/SimpleSAML/Metadata/test-metadata/source3/saml20-sp-remote.d/entityC.php @@ -0,0 +1,21 @@ + 'entityC', + 'name' => + [ + 'en' => 'entityC SP from source3', + ], + 'metadata-set' => 'saml20-sp-remote', + 'AssertionConsumerService' => + [ + 0 => + [ + 'Binding' => 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST', + 'Location' => 'https://entityC.example.org/Shibboleth.sso/SAML2/POST', + 'index' => 1, + 'isDefault' => true, + ], + ] +]; +