Skip to content

Commit 7ccf448

Browse files
Koen Cornelisjohanib
authored andcommitted
Extract ConsentHashService from legacy consent model
Prior to this change, all consent hashing and persistence logic lived inside the legacy Corto consent model (a God object). This commit extracts a dedicated ConsentHashService that encapsulates both the existing (unstable) hashing algorithm and a new stable hashing algorithm. - Add ConsentHashService with both old and stable hashing methods - Move DB consent queries to the existing DbalConsentRepository, applying naming conventions to query methods - Extract ConsentHashServiceInterface to allow mocking in tests (the concrete class is final) - Wire up service configuration Pivotal ticket: https://www.pivotaltracker.com/story/show/176513931
1 parent 5c3ca2a commit 7ccf448

17 files changed

Lines changed: 842 additions & 150 deletions

File tree

config/services/compat.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ services:
5050
class: EngineBlock_Corto_Model_Consent_Factory
5151
arguments:
5252
- "@engineblock.compat.corto_filter_command_factory"
53-
- "@engineblock.compat.database_connection_factory"
53+
- "@engineblock.service.consent.ConsentHashService"
5454

5555
engineblock.compat.saml2_id_generator:
5656
public: true

config/services/services.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ services:
7474
- '@OpenConext\EngineBlock\Metadata\LoaRepository'
7575
- '@logger'
7676

77+
engineblock.service.consent.ConsentHashService:
78+
class: OpenConext\EngineBlock\Service\Consent\ConsentHashService
79+
public: false
80+
7781
OpenConext\EngineBlock\Service\ConsentService:
7882
arguments:
7983
- '@OpenConext\EngineBlockBundle\Authentication\Repository\DbalConsentRepository'

library/EngineBlock/Application/DiContainer.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public function getAuthenticationLoopGuard()
161161
}
162162

163163
/**
164-
* @return OpenConext\EngineBlock\Service\ConsentService
164+
* @return OpenConext\EngineBlock\Service\Consent\ConsentService
165165
*/
166166
public function getConsentService()
167167
{

library/EngineBlock/Corto/Model/Consent.php

Lines changed: 50 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Doctrine\DBAL\Statement;
2020
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
2121
use OpenConext\EngineBlock\Authentication\Value\ConsentType;
22+
use OpenConext\EngineBlock\Service\Consent\ConsentHashServiceInterface;
2223

2324
class EngineBlock_Corto_Model_Consent
2425
{
@@ -37,15 +38,10 @@ class EngineBlock_Corto_Model_Consent
3738
*/
3839
private $_response;
3940
/**
40-
* @var array
41+
* @var array All attributes as an associative array.
4142
*/
4243
private $_responseAttributes;
4344

44-
/**
45-
* @var EngineBlock_Database_ConnectionFactory
46-
*/
47-
private $_databaseConnectionFactory;
48-
4945
/**
5046
* A reflection of the eb.run_all_manipulations_prior_to_consent feature flag
5147
*
@@ -61,63 +57,59 @@ class EngineBlock_Corto_Model_Consent
6157
private $_consentEnabled;
6258

6359
/**
64-
* @param string $tableName
65-
* @param bool $mustStoreValues
66-
* @param EngineBlock_Saml2_ResponseAnnotationDecorator $response
67-
* @param array $responseAttributes
68-
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
60+
* @var ConsentHashServiceInterface
61+
*/
62+
private $_hashService;
63+
64+
/**
6965
* @param bool $amPriorToConsentEnabled Is the run_all_manipulations_prior_to_consent feature enabled or not
70-
* @param bool $consentEnabled Is the feature_enable_consent feature enabled or not
7166
*/
7267
public function __construct(
73-
$tableName,
74-
$mustStoreValues,
68+
string $tableName,
69+
bool $mustStoreValues,
7570
EngineBlock_Saml2_ResponseAnnotationDecorator $response,
7671
array $responseAttributes,
77-
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory,
78-
$amPriorToConsentEnabled,
79-
$consentEnabled
80-
)
81-
{
72+
bool $amPriorToConsentEnabled,
73+
bool $consentEnabled,
74+
ConsentHashServiceInterface $hashService
75+
) {
8276
$this->_tableName = $tableName;
8377
$this->_mustStoreValues = $mustStoreValues;
8478
$this->_response = $response;
8579
$this->_responseAttributes = $responseAttributes;
86-
$this->_databaseConnectionFactory = $databaseConnectionFactory;
8780
$this->_amPriorToConsentEnabled = $amPriorToConsentEnabled;
81+
$this->_hashService = $hashService;
8882
$this->_consentEnabled = $consentEnabled;
8983
}
9084

91-
public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider)
85+
public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
9286
{
9387
return !$this->_consentEnabled ||
9488
$this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
9589
}
9690

97-
public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider)
91+
public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
9892
{
9993
return !$this->_consentEnabled ||
10094
$this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
10195
}
10296

103-
public function giveExplicitConsentFor(ServiceProvider $serviceProvider)
97+
public function giveExplicitConsentFor(ServiceProvider $serviceProvider): bool
10498
{
10599
return !$this->_consentEnabled ||
106100
$this->_storeConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
107101
}
108102

109-
public function giveImplicitConsentFor(ServiceProvider $serviceProvider)
103+
public function giveImplicitConsentFor(ServiceProvider $serviceProvider): bool
110104
{
111105
return !$this->_consentEnabled ||
112106
$this->_storeConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
113107
}
114108

115-
/**
116-
* @return Doctrine\DBAL\Connection
117-
*/
118-
protected function _getConsentDatabaseConnection()
109+
public function countTotalConsent(): int
119110
{
120-
return $this->_databaseConnectionFactory->create();
111+
$consentUid = $this->_getConsentUid();
112+
return $this->_hashService->countTotalConsent($consentUid);
121113
}
122114

123115
protected function _getConsentUid()
@@ -129,116 +121,55 @@ protected function _getConsentUid()
129121
return $this->_response->getNameIdValue();
130122
}
131123

132-
protected function _getAttributesHash($attributes)
124+
protected function _getAttributesHash($attributes): string
133125
{
134-
$hashBase = NULL;
135-
if ($this->_mustStoreValues) {
136-
ksort($attributes);
137-
$hashBase = serialize($attributes);
138-
} else {
139-
$names = array_keys($attributes);
140-
sort($names);
141-
$hashBase = implode('|', $names);
142-
}
143-
return sha1($hashBase);
126+
return $this->_hashService->getUnstableAttributesHash($attributes, $this->_mustStoreValues);
144127
}
145128

146-
private function _storeConsent(ServiceProvider $serviceProvider, $consentType)
129+
protected function _getStableAttributesHash($attributes): string
147130
{
148-
$dbh = $this->_getConsentDatabaseConnection();
149-
if (!$dbh) {
150-
return false;
151-
}
131+
return $this->_hashService->getStableAttributesHash($attributes, $this->_mustStoreValues);
132+
}
152133

134+
private function _storeConsent(ServiceProvider $serviceProvider, $consentType): bool
135+
{
153136
$consentUuid = $this->_getConsentUid();
154-
if(! is_string($consentUuid)){
137+
if (!is_string($consentUuid)) {
155138
return false;
156139
}
157140

158-
$query = "INSERT INTO consent (hashed_user_id, service_id, attribute, consent_type, consent_date, deleted_at)
159-
VALUES (?, ?, ?, ?, NOW(), '0000-00-00 00:00:00')
160-
ON DUPLICATE KEY UPDATE attribute=VALUES(attribute), consent_type=VALUES(consent_type), consent_date=NOW()";
161141
$parameters = array(
162142
sha1($consentUuid),
163143
$serviceProvider->entityId,
164-
$this->_getAttributesHash($this->_responseAttributes),
144+
$this->_getStableAttributesHash($this->_responseAttributes),
165145
$consentType,
166146
);
167147

168-
$statement = $dbh->prepare($query);
169-
if (!$statement) {
170-
throw new EngineBlock_Exception(
171-
"Unable to create a prepared statement to insert consent?!",
172-
EngineBlock_Exception::CODE_CRITICAL
173-
);
174-
}
175-
176-
assert($statement instanceof Statement);
177-
try{
178-
foreach ($parameters as $index => $parameter){
179-
$statement->bindValue($index + 1, $parameter);
180-
}
181-
182-
$statement->executeStatement();
183-
}catch (\Doctrine\DBAL\Exception $e){
184-
throw new EngineBlock_Corto_Module_Services_Exception(
185-
sprintf('Error storing consent: "%s"', var_export($e->getMessage(), true)),
186-
EngineBlock_Exception::CODE_CRITICAL
187-
);
188-
}
189-
return true;
148+
return $this->_hashService->storeConsentHash($parameters);
190149
}
191150

192-
private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType)
151+
private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType): bool
193152
{
194-
try {
195-
$dbh = $this->_getConsentDatabaseConnection();
196-
if (!$dbh) {
197-
return false;
198-
}
199-
200-
$attributesHash = $this->_getAttributesHash($this->_responseAttributes);
201-
202-
$consentUuid = $this->_getConsentUid();
203-
if (!is_string($consentUuid)) {
204-
return false;
205-
}
206-
207-
$query = "
208-
SELECT *
209-
FROM {$this->_tableName}
210-
WHERE hashed_user_id = ?
211-
AND service_id = ?
212-
AND attribute = ?
213-
AND consent_type = ?
214-
AND deleted_at IS NULL
215-
";
216-
$hashedUserId = sha1($consentUuid);
217-
$parameters = array(
218-
$hashedUserId,
219-
$serviceProvider->entityId,
220-
$attributesHash,
221-
$consentType,
222-
);
223-
224-
$statement = $dbh->prepare($query);
225-
assert($statement instanceof Statement);
226-
foreach ($parameters as $position => $parameter) {
227-
$statement->bindValue($position + 1, $parameter);
228-
}
229-
$rows = $statement->executeQuery();
230-
231-
if ($rows->rowCount() < 1) {
232-
// No stored consent found
233-
return false;
234-
}
153+
$parameters = array(
154+
sha1($this->_getConsentUid()),
155+
$serviceProvider->entityId,
156+
$this->_getAttributesHash($this->_responseAttributes),
157+
$consentType,
158+
);
159+
160+
$hasUnstableConsentHash = $this->_hashService->retrieveConsentHash($parameters);
235161

162+
if ($hasUnstableConsentHash) {
236163
return true;
237-
} catch (PDOException $e) {
238-
throw new EngineBlock_Corto_ProxyServer_Exception(
239-
sprintf('Consent retrieval failed! Error: "%s"', $e->getMessage()),
240-
EngineBlock_Exception::CODE_ALERT
241-
);
242164
}
165+
166+
$parameters[2] = array(
167+
sha1($this->_getConsentUid()),
168+
$serviceProvider->entityId,
169+
$this->_getStableAttributesHash($this->_responseAttributes),
170+
$consentType,
171+
);
172+
173+
return $this->_hashService->retrieveConsentHash($parameters);
243174
}
244175
}

library/EngineBlock/Corto/Model/Consent/Factory.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
* limitations under the License.
1717
*/
1818

19+
use OpenConext\EngineBlock\Service\Consent\ConsentHashService;
20+
1921
/**
2022
* @todo write a test
2123
*/
@@ -24,21 +26,20 @@ class EngineBlock_Corto_Model_Consent_Factory
2426
/** @var EngineBlock_Corto_Filter_Command_Factory */
2527
private $_filterCommandFactory;
2628

27-
/** @var EngineBlock_Database_ConnectionFactory */
28-
private $_databaseConnectionFactory;
29-
29+
/**
30+
* @var ConsentHashService
31+
*/
32+
private $_hashService;
3033

31-
/**
34+
/**
3235
* @param EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory
33-
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
3436
*/
3537
public function __construct(
3638
EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory,
37-
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
38-
)
39-
{
39+
ConsentHashService $hashService
40+
) {
4041
$this->_filterCommandFactory = $filterCommandFactory;
41-
$this->_databaseConnectionFactory = $databaseConnectionFactory;
42+
$this->_hashService = $hashService;
4243
}
4344

4445
/**
@@ -68,9 +69,9 @@ public function create(
6869
$proxyServer->getConfig('ConsentStoreValues', true),
6970
$response,
7071
$attributes,
71-
$this->_databaseConnectionFactory,
7272
$amPriorToConsent,
73-
$consentEnabled
73+
$consentEnabled,
74+
$this->_hashService
7475
);
7576
}
7677
}

library/EngineBlock/Corto/Module/Service/ProvideConsent.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
use OpenConext\EngineBlock\Metadata\Entity\IdentityProvider;
2020
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
2121
use OpenConext\EngineBlock\Service\AuthenticationStateHelperInterface;
22-
use OpenConext\EngineBlock\Service\ConsentServiceInterface;
22+
use OpenConext\EngineBlock\Service\Consent\ConsentServiceInterface;
2323
use OpenConext\EngineBlock\Service\ProcessingStateHelperInterface;
2424
use OpenConext\EngineBlockBundle\Service\DiscoverySelectionService;
2525
use Psr\Log\LogLevel;

src/OpenConext/EngineBlock/Authentication/Repository/ConsentRepository.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,10 @@ public function findAllFor($userId);
3737
public function deleteAllFor($userId);
3838

3939
public function deleteOneFor(string $userId, string $serviceProviderEntityId): bool;
40+
41+
public function hasConsentHash(array $parameters): bool;
42+
43+
public function storeConsentHash(array $parameters): bool;
44+
45+
public function countTotalConsent($consentUid): int;
4046
}

0 commit comments

Comments
 (0)