diff --git a/config-templates/config.php b/config-templates/config.php index 18bd04bd2948ab9772ed4ebd06a86ecc378c8256..38247d6fdfb57e95fcbe4380fb47f27344a72570 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -410,6 +410,11 @@ $config = array ( 'metadata.sign.privatekey_pass' => NULL, 'metadata.sign.certificate' => NULL, + /* + * This is the default URL to a MetaShare service where a SAML 2.0 IdP can register its metadata. + * This is a highly experimentar feature. + */ + 'metashare.publishurl' => NULL, ); diff --git a/config-templates/metashare.php b/config-templates/metashare.php new file mode 100644 index 0000000000000000000000000000000000000000..f0b7d3c938d7d88343d1a1118f127c912e5365ba --- /dev/null +++ b/config-templates/metashare.php @@ -0,0 +1,45 @@ +<?php + +/* + * Configuration for the MetaShare service. + */ +$config = array( + + /* + * Whether the MetaShare service is enabled. Set to TRUE to enable the MetaShare service. + */ + 'metashare.enable' => FALSE, + + /* + * The path we will store the metadata in. Set this to a directory which is writeable by + * the web server. We will attempt to create this directory if it doesn't exists. + * + * If the path name is relative, it will be interpreted to be relative to the simpleSAMLphp + * directory. + */ + 'metashare.path' => '/tmp/metashare', + + /* + * Whether we should validate the metadata we receive against the schema before allowing them + * to be added. + */ + 'metashare.validateschema' => TRUE, + + /* + * The MetaShare service can optionally sign the list of metadata it generates. Set this to + * TRUE to enable that. + */ + 'metashare.signmetadatalist' => FALSE, + + /* + * When signing metadata, you need to provide a private key and a certificate. You can also + * specify a password for the private key. All paths are relative to the simpleSAMLphp cert + * directory. + */ + 'metashare.privatekey' => NULL, + 'metashare.privatekey_pass' => NULL, + 'metashare.certificate' => NULL, + +); + +?> \ No newline at end of file diff --git a/dictionaries/frontpage.php b/dictionaries/frontpage.php index c6bf364b974fbb66c2e730e4ce0beb3a5d2662fc..8017543865cd42808dfbfa858275bbe17ddad010 100644 --- a/dictionaries/frontpage.php +++ b/dictionaries/frontpage.php @@ -536,6 +536,9 @@ $lang = array( 'hr' => 'OdrĹľavanje i konfiguriranje simpleSAMLphp-a', 'hu' => 'SimpleSAMLphp karbantartása Ă©s beállĂtása', ), + 'link_publish' => array ( + 'en' => 'Publish my SAML 2.0 IdP metadata to the configured MetaShare', + ), ); diff --git a/dictionaries/metashare.php b/dictionaries/metashare.php new file mode 100644 index 0000000000000000000000000000000000000000..0b53567e9ade29b4e51548a712a55fa331bc39d6 --- /dev/null +++ b/dictionaries/metashare.php @@ -0,0 +1,73 @@ +<?php + +$lang = array( + 'front_header' => array( + 'en' => 'MetaShare', + ), + 'front_desc' => array( + 'en' => 'This is a metadata sharing service. It allows you to add dynamically generated metadata to a shared store.', + ), + 'add_title' => array( + 'en' => 'Add entity', + ), + 'add_desc' => array( + 'en' => 'Add new or updated metadata by specifying the URL of the metadata. This URL must match the entity identifier of the entity described in the metadata.', + ), + 'add_entityid' => array( + 'en' => 'Entity identifier of the entity:', + ), + 'add_do' => array( + 'en' => 'Add', + ), + 'downloadall_desc' => array( + 'en' => 'It is possible to download all the metadata as a single XML file. This file will contain a single EntitiesDescriptor which contains all the entities which are atted to this MetaShare. The EntitiesDescriptor may be signed by this MetaShare if that is enabled in the configuration.', + ), + 'downloadall_link' => array( + 'en' => 'Download all metadata', + ), + 'entities_title' => array( + 'en' => 'Entities', + ), + 'entities_desc' => array( + 'en' => 'This is a list of all the entities which are currently stored in this MetaShare. Click on a link to download the metadata of the given entity.', + ), + 'entities_empty' => array( + 'en' => 'No entities are currently stored in this MetaShare.', + ), + 'text' => array( + 'en' => 'text', + ), + 'addpage_header' => array( + 'en' => 'Add metadata', + ), + 'addpage_ok' => array( + 'en' => 'The metadata from "%URL%" was successfylly added.', + ), + 'addpage_nourl' => array( + 'en' => 'No URL parameter given.', + ), + 'addpage_invalidurl' => array( + 'en' => 'Invalid URL/entity id to metadata. The entity id should be a valid http: or https: URL. The URL you gave was "%URL%".', + ), + 'addpage_nodownload' => array( + 'en' => 'Unable to download metadata from "%URL%".', + ), + 'addpage_invalidxml' => array( + 'en' => 'Malformed XML in metadata. The URL you gave was "%URL%".', + ), + 'addpage_notentitydescriptor' => array( + 'en' => 'The root node of the metadata was not an EntityDescriptor element. The URL you gave was "%URL%".', + ), + 'addpage_entityid' => array( + 'en' => 'The entity identifier in the metadata did not match the URL of the metadata ("%URL%").', + ), + 'addpage_validation' => array( + 'en' => 'XML validation of the metadata from "%URL%" failed:', + ), + 'addpage_gofront' => array( + 'en' => 'Go to metadata list', + ), + +); + +?> \ No newline at end of file diff --git a/lib/SimpleSAML/MetaShare/Store.php b/lib/SimpleSAML/MetaShare/Store.php new file mode 100644 index 0000000000000000000000000000000000000000..7e476b177552f922db3941e892b8c0c957905c53 --- /dev/null +++ b/lib/SimpleSAML/MetaShare/Store.php @@ -0,0 +1,184 @@ +<?php + +/** + * This class contains accessor-functions for listing and manipulating + * the metadata which is stored by the MetaShare part of simpleSAMLphp. + * + * @author Olav Morken, UNINETT AS. + * @package simpleSAMLphp + * @version $Id$ + */ +class SimpleSAML_MetaShare_Store { + + /** + * The singleton instance of this class. + */ + private static $instance = NULL; + + + /** + * The directory we store metadata in. This path newer ends with a slash. + */ + private $metadataPath; + + + /** + * Initializes the SimpleSAML_MetaShare_Store object. Only called by the getInstance + * singleton accessor. + */ + private function __construct() { + $metaConfig = SimpleSAML_Configuration::getInstance()->copyFromBase('metashare', 'metashare.php'); + $this->metadataPath = $metaConfig->getString('metashare.path'); + $this->metadataPath = SimpleSAML_Utilities::resolvePath($this->metadataPath); + + if(!is_dir($this->metadataPath)) { + $ret = mkdir($this->metadataPath, 0755, TRUE); + if(!$ret) { + throw new Exception('Unable to create directory: ' . $this->metadataPath); + } + } + } + + + /** + * Singleton accessor for the SimpleSAML_MetaShare_Store object. Will create a new instance + * of the object if no instance exsists. If an instance already exists, this function will + * return that. + * + * @return The SimpleSAML_MetaShare_Store object. + */ + public static function getInstance() { + if(self::$instance === NULL) { + self::$instance = new self(); + } + + return self::$instance; + } + + + /** + * Get the filename (with path) for a given entity id. + * + * @param $entityId The entity id. + * @return The absolute path to where the file for the given entity id should be. + */ + private function entityIdToPath($entityId) { + assert('is_string($entityId)'); + + /* We urlencode the entity id to remove slashes and other troublesome characters. */ + return $this->metadataPath . '/' . urlencode($entityId) . '.xml'; + } + + + /** + * Get the entity id for a given path. + * + * @param $path The path to the file. We only look at the filename part of the path. + * @return The entity id that the path represents, or FALSE if we don't believe that it represents + * an entity id. + */ + private function pathToEntityId($path) { + assert('is_string($path)'); + + $filename = basename($path); + + /* The filename should end with '.xml' */ + if(substr($filename, -4) !== '.xml') { + return FALSE; + } + + $entityId = urldecode(substr($filename, 0, -4)); + return $entityId; + } + + + /** + * Add metadata to the metadata store. Will throw an exception if an error occurs. + * + * This function expects the metadata which is added to be valid. + * + * @param $metadata The metadata in the form of a DOMElement which represents the + * EntityDescriptor of the metadata. + */ + public function addMetadata($metadata) { + assert('$metadata instanceof DOMElement'); + assert('SimpleSAML_Utilities::isDOMElementOfType($metadata, "EntityDescriptor", "@md")'); + + /* We create a new DOMDocument from the metadata. This way we can manipulate it in any way + * we want, without affecting anything else. We can also enforce the character set of the + * resulting XML. + */ + $doc = new DOMDocument('1.0', 'utf-8'); + $metadata = $doc->importNode($metadata, TRUE); + $doc->appendChild($metadata); + + $entityId = $metadata->getAttribute('entityID'); + $filePath = $this->entityIdToPath($entityId); + + $xml = $doc->saveXML(); + if($xml === FALSE) { + throw new Exception('Unable to build XML string from metadata.'); + } + + /* Save it to the file. */ + $ret = file_put_contents($filePath, $xml); + if($ret === FALSE) { + throw new Exception('Unable to save the metadata to ' . $filePath); + } + } + + + /** + * Retrieve metadata from the metadata store. + * + * @param $entityId The entity id whose metadata we should find. + * @return The metadata as a DOMElement, or FALSE if we are unable to locate or load the given metadata. + */ + public function getMetadata($entityId) { + assert('is_string($entityId)'); + + $filePath = $this->entityIdToPath($entityId); + $xmlString = file_get_contents($filePath); + if($xmlString === FALSE) { + return FALSE; + } + + $doc = new DOMDocument(); + $ret = $doc->loadXML($xmlString); + if(!$ret) { + return FALSE; + } + + assert('SimpleSAML_Utilities::isDOMElementOfType($doc->firstChild, "EntityDescriptor", "@md")'); + return $doc->firstChild; + } + + + /** + * Retrieve a list of the entities which are stored in the MetaShare store. + * + * @return An array with the entity ids of all the entities which are stored in the + * MetaShare store. + */ + public function getEntityList() { + $entities = array(); + + $dir = opendir($this->metadataPath); + if($dir === FALSE) { + throw new Exception('Unable to open the MetaShare directory: ' . $this->metadataPath); + } + + while( ($name = readdir($dir)) !== FALSE) { + $entityId = $this->pathToEntityId($name); + if($entityId !== FALSE) { + $entities[] = $entityId; + } + } + + closedir($dir); + + return $entities; + } +} + +?> \ No newline at end of file diff --git a/templates/default/metashare-add.php b/templates/default/metashare-add.php new file mode 100644 index 0000000000000000000000000000000000000000..47b3de7d4177b9fb4fda01014a16fd8a54518dd7 --- /dev/null +++ b/templates/default/metashare-add.php @@ -0,0 +1,24 @@ +<?php +$this->data['header'] = $this->t('addpage_header'); +$this->includeAtTemplateBase('includes/header.php'); + +echo('<div id="content">'); +echo('<h2>' . $this->t('addpage_header') . '</h2>'); + +$url = $this->data['url']; +$status = $this->data['status']; + +$replaceurl = array('%URL%' => htmlspecialchars($this->data['url'])); + +echo('<p>' . $this->t('addpage_' . $status, TRUE, TRUE, $replaceurl) . '</p>'); + + +if(array_key_exists('errortext', $this->data)) { + echo('<pre>' . htmlspecialchars($this->data['errortext']) . '</pre>'); +} + +echo('<p><a href="index.php">' . $this->t('addpage_gofront') . '</a></p>'); + +$this->includeAtTemplateBase('includes/footer.php'); + +?> \ No newline at end of file diff --git a/templates/default/metashare-list.php b/templates/default/metashare-list.php new file mode 100644 index 0000000000000000000000000000000000000000..fb26ef98774caa77c61d65519dac1edd91ead0f8 --- /dev/null +++ b/templates/default/metashare-list.php @@ -0,0 +1,41 @@ +<?php +$this->data['header'] = $this->t('front_header'); +$this->includeAtTemplateBase('includes/header.php'); + +echo('<div id="content">'); +echo('<h2>' . $this->t('front_header') . '</h2>'); +echo('<p>' . $this->t('front_desc') . '</p>'); + +echo('<h3>' . $this->t('add_title') . '</h3>'); +echo('<p>' . $this->t('add_desc') . '</p>'); +echo('<form action="add.php">'); +echo('<p>'); +echo($this->t('add_entityid') . '<br/>'); +echo('<input type="text" name="url" size="70" />'); +echo('<input type="submit" value="' . $this->t('add_do') . '" />'); +echo('</p>'); +echo('</form>'); + +echo('<h3>' . $this->t('entities_title') . '</h3>'); +if(count($this->data['entities']) > 0) { + echo('<p>' . $this->t('downloadall_desc') . '</p>'); + echo('<p><a href="downloadall.php">' . $this->t('downloadall_link') . + '</a> [<a href="downloadall.php?mimetype=text/plain">' . $this->t('text') . '</a>]</p>'); + echo('<p>' . $this->t('entities_desc') . '</p>'); + echo('<ul>'); + foreach($this->data['entities'] as $entityId) { + $dllink = 'download.php?entityid=' . urlencode($entityId); + echo('<li>'); + echo('<a href="' . htmlspecialchars($dllink) . '">' . + htmlspecialchars($entityId) . '</a>'); + echo(' [<a href="' . htmlspecialchars($dllink . '&mimetype=text/plain') . '">' . + $this->t('text') . '</a>]'); + echo('</li>'); + } + echo('</ul>'); +} else { + echo('<p>' . $this->t('entities_empty') . '</p>'); +} + +$this->includeAtTemplateBase('includes/footer.php'); +?> \ No newline at end of file diff --git a/www/index.php b/www/index.php index 242715de871db9663664d00f132cc5a2db5e923c..eb68e42690e9a0169a9973339920a018a73dac9f 100644 --- a/www/index.php +++ b/www/index.php @@ -60,6 +60,18 @@ if($config->getBoolean('idpdisco.enableremember', FALSE)) { ); } +if ($config->getValue('enable.saml20-idp') === TRUE) { + $publishURL = $config->getString('metashare.publishurl', NULL); + if ($publishURL !== NULL) { + $metadataURL = SimpleSAML_Utilities::resolveURL('saml2/idp/metadata.php'); + $publishURL = SimpleSAML_Utilities::addURLparameter($publishURL, 'url=' . urlencode($metadataURL)); + $links[] = array( + 'href' => $publishURL, + 'text' => 'link_publish', + ); + } +} + $linksmeta = array(); diff --git a/www/metashare/add.php b/www/metashare/add.php new file mode 100644 index 0000000000000000000000000000000000000000..dd658db723dfc32b792683f6fe4d2f8250315a09 --- /dev/null +++ b/www/metashare/add.php @@ -0,0 +1,104 @@ +<?php + +require_once('../_include.php'); + +/** + * This page handles adding of metadata. + */ + +$config = SimpleSAML_Configuration::getInstance(); +$metaConfig = $config->copyFromBase('metashare', 'metashare.php'); + +if(!$metaConfig->getBoolean('metashare.enable', FALSE)) { + header('HTTP/1.0 401 Forbidden'); + $session = SimpleSAML_Session::getInstance(); + SimpleSAML_Utilities::fatalError($session->getTrackID(), 'NOACCESS'); +} + +$store = SimpleSAML_MetaShare_Store::getInstance(); +$t = new SimpleSAML_XHTML_Template($config, 'metashare-add.php', 'metashare.php'); + + +if(!array_key_exists('url', $_GET) || empty($_GET['url'])) { + $t->data['url'] = NULL; + $t->data['status'] = 'nourl'; + $t->show(); + exit; +} + +$url = $_GET['url']; +$t->data['url'] = $url; + +/* We accept http or https URLs */ +if(substr($url, 0, 7) !== 'http://' && substr($url, 0, 8) !== 'https://') { + $t->data['status'] = 'invalidurl'; + $t->show(); + exit(); +} + +/* Attempt to download the metadata. */ +$metadata = file_get_contents($url); +if($metadata === FALSE) { + $t->data['status'] = 'nodownload'; + $t->show(); + exit(); +} + +/* Load the metadata into an XML document. */ +SimpleSAML_XML_Errors::begin(); +$doc = new DOMDocument(); +$doc->validateOnParse = FALSE; +$doc->strictErrorChecking = TRUE; +try { + $ok = $doc->loadXML($metadata); + if($ok !== TRUE) { + $doc = NULL; + } +} catch(DOMException $e) { + $doc = NULL; +} +$errors = SimpleSAML_XML_Errors::end(); +if($doc === NULL || count($errors) > 0) { + $t->data['status'] = 'invalidxml'; + $t->data['errortext'] = SimpleSAML_XML_Errors::formatErrors($errors); + $t->show(); + exit(); +} +$metadata = $doc->firstChild; + +/* Check that the metadata is an EntityDescriptor */ +if(!SimpleSAML_Utilities::isDOMElementOfType($metadata, 'EntityDescriptor', '@md')) { + $t->data['status'] = 'notentitydescriptor'; + $t->show(); + exit(); +} + +/* Check that the entity id of the metadata matches the URL. */ +$entityId = $metadata->getAttribute('entityID'); +if($entityId !== $url) { + $t->data['status'] = 'entityid'; + $t->data['errortext'] = 'Entity id: ' . $entityId . "\n" . 'URL: ' . $url . "\n"; + $t->show(); + exit(); +} + +/* Validate the metadata against the metadata schema (if enabled). */ +if($metaConfig->getBoolean('metashare.validateschema')) { + $errors = SimpleSAML_Utilities::validateXML($doc, 'saml-schema-metadata-2.0.xsd'); + if($errors !== '') { + $t->data['status'] = 'validation'; + $t->data['errortext'] = $errors; + $t->show(); + exit(); + } +} + + +/* Add the metadata to the metadata store. */ +$store->addMetadata($metadata); + +$t->data['status'] = 'ok'; +$t->show(); +exit(); + +?> \ No newline at end of file diff --git a/www/metashare/download.php b/www/metashare/download.php new file mode 100644 index 0000000000000000000000000000000000000000..5b50e1abeddb89aecd56a495d634cd67d255c521 --- /dev/null +++ b/www/metashare/download.php @@ -0,0 +1,68 @@ +<?php + +require_once('../_include.php'); + +/** + * This page handles downloading of single metadata entries from the MetaShare. + */ + +$metaConfig = SimpleSAML_Configuration::getInstance()->copyFromBase('metashare', 'metashare.php'); + +if(!$metaConfig->getBoolean('metashare.enable', FALSE)) { + header('HTTP/1.0 401 Forbidden'); + header('Content-Type: text/plain'); + + echo("The MetaShare service is disabled.\n"); + exit(); +} + +/** + * This function shows a minimal 404 Not Found page. + * + * This function newer returns. + * + * @param $entityId The entity identifier which was not found. Can be NULL, + */ +function showNotFound($entityId) { + + header('HTTP/1.0 404 Not Found'); + header('Content-Type: text/plain'); + + echo("Could not find the given entity id.\n"); + + + if($entityId === NULL) { + echo("No entity id given.\n"); + } else { + echo('Entity id: ' . $entityId . "\n"); + } + + exit(); +} + + +if(!array_key_exists('entityid', $_GET)) { + showNotFound(NULL); +} +$entityId = $_GET['entityid']; + + +/* Load the metadata. */ +$store = SimpleSAML_MetaShare_Store::getInstance(); +$metadata = $store->getMetadata($entityId); +if($metadata === FALSE) { + showNotFound($entityId); +} + + +/* Show the metadata. */ +if(array_key_exists('mimetype', $_GET)) { + $mimeType = $_GET['mimetype']; +} else { + $mimeType = 'application/samlmetadata+xml'; +} +header('Content-Type: ' . $mimeType); + +echo($metadata->ownerDocument->saveXML($metadata)); + +?> \ No newline at end of file diff --git a/www/metashare/downloadall.php b/www/metashare/downloadall.php new file mode 100644 index 0000000000000000000000000000000000000000..8eb5815f414ba1d5047595cfa7866a7013edd74d --- /dev/null +++ b/www/metashare/downloadall.php @@ -0,0 +1,66 @@ +<?php + +require_once('../_include.php'); + +/** + * This page handles downloading of all metadata entries from the MetaShare. + */ + +$metaConfig = SimpleSAML_Configuration::getInstance()->copyFromBase('metashare', 'metashare.php'); + +if(!$metaConfig->getBoolean('metashare.enable', FALSE)) { + header('HTTP/1.0 401 Forbidden'); + header('Content-Type: text/plain'); + + echo("The MetaShare service is disabled.\n"); + exit(); +} + +/* Build EntitiesDescriptor. */ + +$doc = new DOMDocument('1.0', 'utf-8'); +$root = $doc->createElementNS('urn:oasis:names:tc:SAML:2.0:metadata', 'EntitiesDescriptor'); +$doc->appendChild($root); + +$store = SimpleSAML_MetaShare_Store::getInstance(); +foreach($store->getEntityList() as $entityId) { + $entityNode = $store->getMetadata($entityId); + if($entityNode === FALSE) { + /* For some reason we were unable to load the metadata - skip entity. */ + continue; + } + + $entityNode = $doc->importNode($entityNode, TRUE); + assert($entityNode !== FALSE); + + $root->appendChild($entityNode); +} + + +/* Sign the metadata if enabled. */ +if($metaConfig->getBoolean('metashare.signmetadatalist', FALSE)) { + $privateKey = $metaConfig->getString('metashare.privatekey'); + $privateKeyPass = $metaConfig->getString('metashare.privatekey_pass', NULL); + $certificate = $metaConfig->getString('metashare.certificate'); + + $signer = new SimpleSAML_XML_Signer(array( + 'privatekey' => $privateKey, + 'privatekey_pass' => $privateKeyPass, + 'certificate' => $certificate, + 'id' => 'ID', + )); + $signer->sign($root, $root, $root->firstChild); +} + + +/* Show the metadata. */ +if(array_key_exists('mimetype', $_GET)) { + $mimeType = $_GET['mimetype']; +} else { + $mimeType = 'application/samlmetadata+xml'; +} +header('Content-Type: ' . $mimeType); + +echo($doc->saveXML()); + +?> \ No newline at end of file diff --git a/www/metashare/index.php b/www/metashare/index.php new file mode 100644 index 0000000000000000000000000000000000000000..1f52caebd392991b932b4310252396c90fc33bb1 --- /dev/null +++ b/www/metashare/index.php @@ -0,0 +1,27 @@ +<?php + +require_once('../_include.php'); + +/** + * This page lists all metadata currently stored in the MetaShare store. + */ + +$config = SimpleSAML_Configuration::getInstance(); +$metaConfig = $config->copyFromBase('metashare', 'metashare.php'); + +if(!$metaConfig->getBoolean('metashare.enable', FALSE)) { + header('HTTP/1.0 401 Forbidden'); + $session = SimpleSAML_Session::getInstance(); + SimpleSAML_Utilities::fatalError($session->getTrackID(), 'NOACCESS'); +} + +$store = SimpleSAML_MetaShare_Store::getInstance(); +$entities = $store->getEntityList(); + +$t = new SimpleSAML_XHTML_Template($config, 'metashare-list.php', 'metashare.php'); +$t->data['entities'] = $entities; +$t->show(); +exit; + + +?> \ No newline at end of file