Skip to content
30 changes: 30 additions & 0 deletions database/DoctrineMigrations/Version20220503092351.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php declare(strict_types=1);

namespace OpenConext\EngineBlock\Doctrine\Migrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Change to the consent schema
* 1. Added the `attribute_stable` column, string(80), not null
* 2. Changed the `attribute` column, has been made nullable
*/
final class Version20220503092351 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('ALTER TABLE consent ADD attribute_stable VARCHAR(80) NOT NULL, CHANGE attribute attribute VARCHAR(80) DEFAULT NULL');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not yet decided on the pros and cons of adding a new hash field next to the old field.

I believe the problem is solvable with just one database field. Not sure yet if that is preferable though.

Copy link
Member

@MKodde MKodde May 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm open for other suggestions! Having it in one single column will work too in my opinion. The 'OR' based query solution would work just as well on a single column. But would make it harder to decide when we can stop supporting the old style attribute hash method.

}

public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'mysql', 'Migration can only be executed safely on \'mysql\'.');

$this->addSql('ALTER TABLE consent DROP attribute_stable, CHANGE attribute attribute VARCHAR(80) CHARACTER SET utf8 NOT NULL COLLATE `utf8_unicode_ci`');
}
}
2 changes: 1 addition & 1 deletion library/EngineBlock/Application/DiContainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ public function getAuthenticationLoopGuard()
}

/**
* @return OpenConext\EngineBlock\Service\ConsentService
* @return OpenConext\EngineBlock\Service\Consent\ConsentService
*/
public function getConsentService()
{
Expand Down
187 changes: 76 additions & 111 deletions library/EngineBlock/Corto/Model/Consent.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
* limitations under the License.
*/

use OpenConext\EngineBlock\Authentication\Value\ConsentVersion;
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
use OpenConext\EngineBlock\Authentication\Value\ConsentType;
use OpenConext\EngineBlock\Service\Consent\ConsentHashServiceInterface;

class EngineBlock_Corto_Model_Consent
{
Expand All @@ -36,15 +38,10 @@ class EngineBlock_Corto_Model_Consent
*/
private $_response;
/**
* @var array
* @var array All attributes as an associative array.
*/
private $_responseAttributes;

/**
* @var EngineBlock_Database_ConnectionFactory
*/
private $_databaseConnectionFactory;

/**
* A reflection of the eb.run_all_manipulations_prior_to_consent feature flag
*
Expand All @@ -60,63 +57,78 @@ class EngineBlock_Corto_Model_Consent
private $_consentEnabled;

/**
* @param string $tableName
* @param bool $mustStoreValues
* @param EngineBlock_Saml2_ResponseAnnotationDecorator $response
* @param array $responseAttributes
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
* @var ConsentHashServiceInterface
*/
private $_hashService;

/**
* @param bool $amPriorToConsentEnabled Is the run_all_manipulations_prior_to_consent feature enabled or not
* @param bool $consentEnabled Is the feature_enable_consent feature enabled or not
*/
public function __construct(
$tableName,
$mustStoreValues,
string $tableName,
bool $mustStoreValues,
EngineBlock_Saml2_ResponseAnnotationDecorator $response,
array $responseAttributes,
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory,
$amPriorToConsentEnabled,
$consentEnabled
)
{
bool $amPriorToConsentEnabled,
bool $consentEnabled,
ConsentHashServiceInterface $hashService
) {
$this->_tableName = $tableName;
$this->_mustStoreValues = $mustStoreValues;
$this->_response = $response;
$this->_responseAttributes = $responseAttributes;
$this->_databaseConnectionFactory = $databaseConnectionFactory;
$this->_amPriorToConsentEnabled = $amPriorToConsentEnabled;
$this->_hashService = $hashService;
$this->_consentEnabled = $consentEnabled;
}

public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider)
public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
{
return !$this->_consentEnabled ||
$this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
if (!$this->_consentEnabled) {
return true;
}
$consent = $this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
return $consent->given();
}

/**
* Although the user has given consent previously we want to upgrade the deprecated unstable consent
* to the new stable consent type.
* https://www.pivotaltracker.com/story/show/176513931
*/
public function upgradeAttributeHashFor(ServiceProvider $serviceProvider, string $consentType): void
{
$consentVersion = $this->_hasStoredConsent($serviceProvider, $consentType);
if ($consentVersion->isUnstable()) {
$this->_updateConsent($serviceProvider, $consentType);
}
}

public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider)
public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
{
return !$this->_consentEnabled ||
$this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
if (!$this->_consentEnabled) {
return true;
}
$consent = $this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
return $consent->given();
}

public function giveExplicitConsentFor(ServiceProvider $serviceProvider)
public function giveExplicitConsentFor(ServiceProvider $serviceProvider): bool
{
return !$this->_consentEnabled ||
$this->_storeConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
}

public function giveImplicitConsentFor(ServiceProvider $serviceProvider)
public function giveImplicitConsentFor(ServiceProvider $serviceProvider): bool
{
return !$this->_consentEnabled ||
$this->_storeConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
}

/**
* @return bool|PDO
*/
protected function _getConsentDatabaseConnection()
public function countTotalConsent(): int
{
return $this->_databaseConnectionFactory->create();
$consentUid = $this->_getConsentUid();
return $this->_hashService->countTotalConsent($consentUid);
}

protected function _getConsentUid()
Expand All @@ -128,98 +140,51 @@ protected function _getConsentUid()
return $this->_response->getNameIdValue();
}

protected function _getAttributesHash($attributes)
protected function _getAttributesHash($attributes): string
{
$hashBase = NULL;
if ($this->_mustStoreValues) {
ksort($attributes);
$hashBase = serialize($attributes);
} else {
$names = array_keys($attributes);
sort($names);
$hashBase = implode('|', $names);
}
return sha1($hashBase);
return $this->_hashService->getUnstableAttributesHash($attributes, $this->_mustStoreValues);
}

private function _storeConsent(ServiceProvider $serviceProvider, $consentType)
protected function _getStableAttributesHash($attributes): string
{
$dbh = $this->_getConsentDatabaseConnection();
if (!$dbh) {
return false;
}
return $this->_hashService->getStableAttributesHash($attributes, $this->_mustStoreValues);
}

$query = "INSERT INTO consent (hashed_user_id, service_id, attribute, consent_type, consent_date, deleted_at)
VALUES (?, ?, ?, ?, NOW(), '0000-00-00 00:00:00')
ON DUPLICATE KEY UPDATE attribute=VALUES(attribute), consent_type=VALUES(consent_type), consent_date=NOW()";
private function _storeConsent(ServiceProvider $serviceProvider, $consentType): bool
{
$parameters = array(
sha1($this->_getConsentUid()),
$serviceProvider->entityId,
$this->_getAttributesHash($this->_responseAttributes),
$this->_getStableAttributesHash($this->_responseAttributes),
$consentType,
);

$statement = $dbh->prepare($query);
if (!$statement) {
throw new EngineBlock_Exception(
"Unable to create a prepared statement to insert consent?!",
EngineBlock_Exception::CODE_CRITICAL
);
}

/** @var $statement PDOStatement */
if (!$statement->execute($parameters)) {
throw new EngineBlock_Corto_Module_Services_Exception(
sprintf('Error storing consent: "%s"', var_export($statement->errorInfo(), true)),
EngineBlock_Exception::CODE_CRITICAL
);
}
return true;
return $this->_hashService->storeConsentHash($parameters);
}

private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType)
private function _updateConsent(ServiceProvider $serviceProvider, $consentType): bool
{
try {
$dbh = $this->_getConsentDatabaseConnection();
if (!$dbh) {
return false;
}

$attributesHash = $this->_getAttributesHash($this->_responseAttributes);

$query = "
SELECT *
FROM {$this->_tableName}
WHERE hashed_user_id = ?
AND service_id = ?
AND attribute = ?
AND consent_type = ?
AND deleted_at IS NULL
";
$hashedUserId = sha1($this->_getConsentUid());
$parameters = array(
$hashedUserId,
$serviceProvider->entityId,
$attributesHash,
$consentType,
);

/** @var $statement PDOStatement */
$statement = $dbh->prepare($query);
$statement->execute($parameters);
$rows = $statement->fetchAll();

if (count($rows) < 1) {
// No stored consent found
return false;
}
$parameters = array(
$this->_getStableAttributesHash($this->_responseAttributes),
$this->_getAttributesHash($this->_responseAttributes),
sha1($this->_getConsentUid()),
$serviceProvider->entityId,
$consentType,
);

return true;
} catch (PDOException $e) {
throw new EngineBlock_Corto_ProxyServer_Exception(
sprintf('Consent retrieval failed! Error: "%s"', $e->getMessage()),
EngineBlock_Exception::CODE_ALERT
);
}
return $this->_hashService->updateConsentHash($parameters);
}

private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType): ConsentVersion
{
$consentUuid = sha1($this->_getConsentUid());
$parameters = [
$consentUuid,
$serviceProvider->entityId,
$this->_getAttributesHash($this->_responseAttributes),
$this->_getStableAttributesHash($this->_responseAttributes),
$consentType,
];
return $this->_hashService->retrieveConsentHash($parameters);
}
}
23 changes: 12 additions & 11 deletions library/EngineBlock/Corto/Model/Consent/Factory.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
* limitations under the License.
*/

use OpenConext\EngineBlock\Service\Consent\ConsentHashService;

/**
* @todo write a test
*/
Expand All @@ -24,21 +26,20 @@ class EngineBlock_Corto_Model_Consent_Factory
/** @var EngineBlock_Corto_Filter_Command_Factory */
private $_filterCommandFactory;

/** @var EngineBlock_Database_ConnectionFactory */
private $_databaseConnectionFactory;

/**
* @var ConsentHashService
*/
private $_hashService;

/**
/**
* @param EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
*/
public function __construct(
EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory,
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
)
{
ConsentHashService $hashService
) {
$this->_filterCommandFactory = $filterCommandFactory;
$this->_databaseConnectionFactory = $databaseConnectionFactory;
$this->_hashService = $hashService;
}

/**
Expand Down Expand Up @@ -68,9 +69,9 @@ public function create(
$proxyServer->getConfig('ConsentStoreValues', true),
$response,
$attributes,
$this->_databaseConnectionFactory,
$amPriorToConsent,
$consentEnabled
$consentEnabled,
$this->_hashService
);
}
}
2 changes: 2 additions & 0 deletions library/EngineBlock/Corto/Module/Service/ProcessConsent.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
* limitations under the License.
*/

use OpenConext\EngineBlock\Authentication\Value\ConsentType;
use OpenConext\EngineBlock\Service\AuthenticationStateHelperInterface;
use OpenConext\EngineBlock\Service\ProcessingStateHelperInterface;
use SAML2\Constants;
Expand Down Expand Up @@ -102,6 +103,7 @@ public function serve($serviceName, Request $httpRequest)
if (!$consentRepository->explicitConsentWasGivenFor($serviceProvider)) {
$consentRepository->giveExplicitConsentFor($destinationMetadata);
}
$consentRepository->upgradeAttributeHashFor($destinationMetadata, ConsentType::TYPE_EXPLICIT);

$response->setConsent(Constants::CONSENT_OBTAINED);
$response->setDestination($response->getReturn());
Expand Down
Loading