Skip to content
Snippets Groups Projects
MetaDataStorageHandlerPdo.php 8.9 KiB
Newer Older
declare(strict_types=1);

namespace SimpleSAML\Metadata;

use SimpleSAML\Assert\Assert;
use SimpleSAML\Database;
use SimpleSAML\Error;

/**
 * Class for handling metadata files stored in a database.
 *
 * This class has been based off a previous version written by
 * mooknarf@gmail.com and patched to work with the latest version
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
 * of SimpleSAMLphp
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
 * @package SimpleSAMLphp
class MetaDataStorageHandlerPdo extends MetaDataStorageSource
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
{
    /**
     * The PDO object
     * @var \SimpleSAML\Database
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    private Database $db;
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

    /**
     * Prefix to apply to the metadata table
     */
    private string $tablePrefix = '';
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

    /**
     * This is an associative array which stores the different metadata sets we have loaded.
     */
    private array $cachedMetadata = [];
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

    /**
     * All the metadata sets supported by this MetaDataStorageHandler
     * @var string[]
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    public array $supportedSets = [
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        'adfs-idp-hosted',
        'adfs-sp-remote',
        'saml20-idp-hosted',
        'saml20-idp-remote',
        'saml20-sp-remote',
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed


    /**
     * This constructor initializes the PDO metadata storage handler with the specified
     * configuration. The configuration is an associative array with the following
     * possible elements (set in config.php):
     * - 'usePersistentConnection': TRUE/FALSE if database connection should be persistent.
     * - 'dsn':                     The database connection string.
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     * - 'username':                Database user name
     * - 'password':                Password for the database user.
     *
     * @param array $config An associative array with the configuration for this handler.
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    public function __construct(/** @scrutinizer ignore-unused */ array $config)
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    {
        $this->db = Database::getInstance();
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    }


    /**
     * This function loads the given set of metadata from a file to a configured database.
     * 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 $metadata Associative array with the metadata, or NULL if we are unable to load
     *     metadata from the given file.
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     *
     * @throws \Exception If a database error occurs.
     * @throws \SimpleSAML\Error\Exception If the metadata can be retrieved from the database, but cannot be decoded.
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    private function load(string $set): ?array
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    {
        $tableName = $this->getTableName($set);

        if (!in_array($set, $this->supportedSets, true)) {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
            return null;
        }

        $stmt = $this->db->read("SELECT entity_id, entity_data FROM $tableName");
        if ($stmt->execute()) {
            $metadata = [];
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

            while ($d = $stmt->fetch()) {
                $data = json_decode($d['entity_data'], true);
                if ($data === null) {
                    throw new Error\Exception("Cannot decode metadata for entity '${d['entity_id']}'");
                }
                if (!array_key_exists('entityid', $data)) {
                    $data['entityid'] = $d['entity_id'];
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
            }

            return $metadata;
        } else {
            throw new \Exception(
                'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
            );
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        }
    }


    /**
     * Retrieve a list of all available metadata for a given set.
     *
     * @param string $set The set we are looking for metadata in.
     *
     * @return array $metadata An associative array with all the metadata for the given set.
     */
    public function getMetadataSet(string $set): array
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    {
        if (array_key_exists($set, $this->cachedMetadata)) {
            return $this->cachedMetadata[$set];
        }

        $metadataSet = $this->load($set);
        if ($metadataSet === null) {
            $metadataSet = [];
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        }

        foreach ($metadataSet as $entityId => &$entry) {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        }

        $this->cachedMetadata[$set] = $metadataSet;
        return $metadataSet;
    }

Tim van Dijen's avatar
Tim van Dijen committed
    /**
Tim van Dijen's avatar
Tim van Dijen committed
     *
     * @param string $entityId The entityId we are looking up.
     * @param string $set The set we are looking for metadata in.
     *
     * @return array|null An associative array with metadata for the given entity, or NULL if we are unable to
    public function getMetaData(string $entityId, string $set): ?array
        if (!in_array($set, $this->supportedSets, true)) {
            return null;
        }

        // support caching
        if (isset($this->cachedMetadata[$entityId][$set])) {
            return $this->cachedMetadata[$entityId][$set];
        }
        $tableName = $this->getTableName($set);
Tim van Dijen's avatar
Tim van Dijen committed
        $stmt = $this->db->read(
            "SELECT entity_id, entity_data FROM {$tableName} WHERE entity_id = :entityId",
            ['entityId' => $entityId]
        );

        // throw pdo exception upon execution failure
        if (!$stmt->execute()) {
            throw new \Exception(
                'PDO metadata handler: Database error: ' . var_export($this->db->getLastError(), true)
            );
        // load the metadata into an array
        $metadataSet = [];
        while ($d = $stmt->fetch()) {
            $data = json_decode($d['entity_data'], true);
            if (json_last_error() != JSON_ERROR_NONE) {
                throw new \SimpleSAML\Error\Exception(
                    "Cannot decode metadata for entity '${d['entity_id']}'"
                );
            // update the entity id to either the key (if not dynamic or generate the dynamic hosted url)
            $data['entityid'] = $entityId;
            $metadataSet[$d['entity_id']] = $data;
        }

        $indexLookup = $this->lookupIndexFromEntityId($entityId, $metadataSet);
        if (isset($indexLookup) && array_key_exists($indexLookup, $metadataSet)) {
            $this->cachedMetadata[$indexLookup][$set] = $metadataSet[$indexLookup];
            return $this->cachedMetadata[$indexLookup][$set];
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    /**
     * Add metadata to the configured database
     *
     * @param string $index Entity ID
     * @param string $set The set to add the metadata to
     * @param array  $entityData Metadata
     *
     * @return bool True/False if entry was successfully added
     */
    public function addEntry(string $index, string $set, array $entityData): bool
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    {
        if (!in_array($set, $this->supportedSets, true)) {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
            return false;
        }

        $tableName = $this->getTableName($set);

        $metadata = $this->db->read(
            "SELECT entity_id, entity_data FROM $tableName WHERE entity_id = :entity_id",
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
                'entity_id' => $index,
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        );

        $retrivedEntityIDs = $metadata->fetch();

Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
            'entity_id'   => $index,
            'entity_data' => json_encode($entityData),
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed

        if ($retrivedEntityIDs !== false && count($retrivedEntityIDs) > 0) {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
                "UPDATE $tableName SET entity_data = :entity_data WHERE entity_id = :entity_id",
                $params
            );
        } else {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
                "INSERT INTO $tableName (entity_id, entity_data) VALUES (:entity_id, :entity_data)",
                $params
            );
        }

Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    }


    /**
     * Replace the -'s to an _ in table names for Metadata sets
     * since SQL does not allow a - in a table name.
     *
     * @param string $table Table
     *
     * @return string Replaced table name
     */
    private function getTableName(string $table): string
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    {
        return $this->db->applyPrefix(str_replace("-", "_", $this->tablePrefix . $table));
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    }


    /**
     * Initialize the configured database
     *
     * @return int|false The number of SQL statements successfully executed, false if some error occurred.
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
     */
    public function initDatabase()
    {
        $driver = $this->db->getDriver();

        $text = 'TEXT';
        if ($driver === 'mysql') {
            $text = 'MEDIUMTEXT';
        }

Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        foreach ($this->supportedSets as $set) {
            $tableName = $this->getTableName($set);
            $rows = $this->db->write(sprintf(
                "CREATE TABLE IF NOT EXISTS $tableName (entity_id VARCHAR(255) PRIMARY KEY NOT NULL, "
                    . "entity_data %s NOT NULL)",
                $text
            ));

Tim van Dijen's avatar
Tim van Dijen committed
            if ($rows === false) {
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
        }
Jaime Perez Crespo's avatar
Jaime Perez Crespo committed
    }