Skip to content

Commit 84fc3b5

Browse files
author
Koen Cornelis
committed
Add a consentHashService
Prior to this commit, there was no service specifically for hashing attribute arrays for consent. This commit adds such a service with both the old hashing algorithm and a stable hashing algorithm. Pivotal ticket: https://www.pivotaltracker.com/story/show/176513931
1 parent 443b903 commit 84fc3b5

8 files changed

Lines changed: 656 additions & 30 deletions

File tree

library/EngineBlock/Corto/Model/Consent.php

Lines changed: 48 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
use OpenConext\EngineBlock\Metadata\Entity\ServiceProvider;
2020
use OpenConext\EngineBlock\Authentication\Value\ConsentType;
21+
use OpenConext\EngineBlock\Service\Consent\ConsentHashService;
2122

2223
class EngineBlock_Corto_Model_Consent
2324
{
@@ -36,7 +37,7 @@ class EngineBlock_Corto_Model_Consent
3637
*/
3738
private $_response;
3839
/**
39-
* @var array
40+
* @var array All attributes as an associative array.
4041
*/
4142
private $_responseAttributes;
4243

@@ -52,21 +53,28 @@ class EngineBlock_Corto_Model_Consent
5253
*/
5354
private $_amPriorToConsentEnabled;
5455

56+
/**
57+
* @var ConsentHashService
58+
*/
59+
private $_hashService;
60+
5561
/**
5662
* @param string $tableName
5763
* @param bool $mustStoreValues
5864
* @param EngineBlock_Saml2_ResponseAnnotationDecorator $response
5965
* @param array $responseAttributes
6066
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
6167
* @param bool $amPriorToConsentEnabled Is the run_all_manipulations_prior_to_consent feature enabled or not
68+
* @param ConsentHashService $hashService
6269
*/
6370
public function __construct(
6471
$tableName,
6572
$mustStoreValues,
6673
EngineBlock_Saml2_ResponseAnnotationDecorator $response,
6774
array $responseAttributes,
6875
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory,
69-
$amPriorToConsentEnabled
76+
$amPriorToConsentEnabled,
77+
$hashService
7078
)
7179
{
7280
$this->_tableName = $tableName;
@@ -75,27 +83,33 @@ public function __construct(
7583
$this->_responseAttributes = $responseAttributes;
7684
$this->_databaseConnectionFactory = $databaseConnectionFactory;
7785
$this->_amPriorToConsentEnabled = $amPriorToConsentEnabled;
86+
$this->_hashService = $hashService;
7887
}
7988

80-
public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider) {
89+
public function explicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
90+
{
8191
return $this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
8292
}
8393

84-
public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider) {
94+
public function implicitConsentWasGivenFor(ServiceProvider $serviceProvider): bool
95+
{
8596
return $this->_hasStoredConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
8697
}
8798

88-
public function giveExplicitConsentFor(ServiceProvider $serviceProvider)
99+
public function giveExplicitConsentFor(ServiceProvider $serviceProvider): bool
89100
{
90101
return $this->_storeConsent($serviceProvider, ConsentType::TYPE_EXPLICIT);
91102
}
92103

93-
public function giveImplicitConsentFor(ServiceProvider $serviceProvider)
104+
public function giveImplicitConsentFor(ServiceProvider $serviceProvider): bool
94105
{
95106
return $this->_storeConsent($serviceProvider, ConsentType::TYPE_IMPLICIT);
96107
}
97108

98-
public function countTotalConsent()
109+
/**
110+
* @throws EngineBlock_Exception
111+
*/
112+
public function countTotalConsent(): int
99113
{
100114
$dbh = $this->_getConsentDatabaseConnection();
101115
$hashedUserId = sha1($this->_getConsentUid());
@@ -129,21 +143,17 @@ protected function _getConsentUid()
129143
return $this->_response->getNameIdValue();
130144
}
131145

132-
protected function _getAttributesHash($attributes)
146+
protected function _getAttributesHash($attributes): string
133147
{
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);
148+
return $this->_hashService->getUnstableAttributesHash($attributes, $this->_mustStoreValues);
149+
}
150+
151+
protected function _getStableAttributesHash($attributes): string
152+
{
153+
return $this->_hashService->getStableAttributesHash($attributes, $this->_mustStoreValues);
144154
}
145155

146-
private function _storeConsent(ServiceProvider $serviceProvider, $consentType)
156+
private function _storeConsent(ServiceProvider $serviceProvider, $consentType): bool
147157
{
148158
$dbh = $this->_getConsentDatabaseConnection();
149159
if (!$dbh) {
@@ -156,7 +166,7 @@ private function _storeConsent(ServiceProvider $serviceProvider, $consentType)
156166
$parameters = array(
157167
sha1($this->_getConsentUid()),
158168
$serviceProvider->entityId,
159-
$this->_getAttributesHash($this->_responseAttributes),
169+
$this->_getStableAttributesHash($this->_responseAttributes),
160170
$consentType,
161171
);
162172

@@ -179,16 +189,27 @@ private function _storeConsent(ServiceProvider $serviceProvider, $consentType)
179189
return true;
180190
}
181191

182-
private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType)
192+
private function _hasStoredConsent(ServiceProvider $serviceProvider, $consentType): bool
183193
{
184-
try {
185-
$dbh = $this->_getConsentDatabaseConnection();
186-
if (!$dbh) {
187-
return false;
188-
}
194+
$dbh = $this->_getConsentDatabaseConnection();
195+
if (!$dbh) {
196+
return false;
197+
}
198+
199+
$unstableConsentHash = $this->_getAttributesHash($this->_responseAttributes);
200+
$hasUnstableConsentHash = $this->retrieveConsentHashFromDb($dbh, $serviceProvider, $consentType, $unstableConsentHash);
189201

190-
$attributesHash = $this->_getAttributesHash($this->_responseAttributes);
202+
if ($hasUnstableConsentHash) {
203+
return true;
204+
}
205+
206+
$stableConsentHash = $this->_getStableAttributesHash($this->_responseAttributes);
207+
return $this->retrieveConsentHashFromDb($dbh, $serviceProvider, $consentType, $stableConsentHash);
208+
}
191209

210+
private function retrieveConsentHashFromDb(PDO $dbh, ServiceProvider $serviceProvider, $consentType, $attributesHash): bool
211+
{
212+
try {
192213
$query = "SELECT * FROM {$this->_tableName} WHERE hashed_user_id = ? AND service_id = ? AND attribute = ? AND consent_type = ?";
193214
$hashedUserId = sha1($this->_getConsentUid());
194215
$parameters = array(

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,24 @@ class EngineBlock_Corto_Model_Consent_Factory
2727
/** @var EngineBlock_Database_ConnectionFactory */
2828
private $_databaseConnectionFactory;
2929

30+
/**
31+
* @var ConsentHashService
32+
*/
33+
private $_hashService;
3034

31-
/**
35+
/**
3236
* @param EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory
3337
* @param EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
3438
*/
3539
public function __construct(
3640
EngineBlock_Corto_Filter_Command_Factory $filterCommandFactory,
37-
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory
41+
EngineBlock_Database_ConnectionFactory $databaseConnectionFactory,
42+
ConsentHashService $hashService
3843
)
3944
{
4045
$this->_filterCommandFactory = $filterCommandFactory;
4146
$this->_databaseConnectionFactory = $databaseConnectionFactory;
47+
$this->_hashService = $hashService;
4248
}
4349

4450
/**
@@ -68,7 +74,8 @@ public function create(
6874
$response,
6975
$attributes,
7076
$this->_databaseConnectionFactory,
71-
$amPriorToConsent
77+
$amPriorToConsent,
78+
$this->_hashService
7279
);
7380
}
7481
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2010 SURFnet B.V.
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
namespace OpenConext\EngineBlock\Service\Consent;
20+
21+
use function array_filter;
22+
use function array_keys;
23+
use function array_values;
24+
use function count;
25+
use function implode;
26+
use function is_array;
27+
use function is_numeric;
28+
use function ksort;
29+
use function serialize;
30+
use function sha1;
31+
use function sort;
32+
use function strtolower;
33+
use function unserialize;
34+
35+
final class ConsentHashService
36+
{
37+
public function getUnstableAttributesHash(array $attributes,bool $mustStoreValues): string
38+
{
39+
$hashBase = null;
40+
if ($mustStoreValues) {
41+
ksort($attributes);
42+
$hashBase = serialize($attributes);
43+
} else {
44+
$names = array_keys($attributes);
45+
sort($names);
46+
$hashBase = implode('|', $names);
47+
}
48+
return sha1($hashBase);
49+
}
50+
51+
public function getStableAttributesHash(array $attributes, bool $mustStoreValues) : string
52+
{
53+
$lowerCasedAttributes = $this->caseNormalizeStringArray($attributes);
54+
$hashBase = $mustStoreValues ? $this->createHashBaseWithValues($lowerCasedAttributes) : $this->createHashBaseWithoutValues($lowerCasedAttributes);
55+
56+
return sha1($hashBase);
57+
}
58+
59+
private function createHashBaseWithValues(array $lowerCasedAttributes): string
60+
{
61+
return serialize($this->sortArray($lowerCasedAttributes));
62+
}
63+
64+
private function createHashBaseWithoutValues(array $lowerCasedAttributes): string
65+
{
66+
$noEmptyAttributes = $this->removeEmptyAttributes($lowerCasedAttributes);
67+
$sortedAttributes = $this->sortArray(array_keys($noEmptyAttributes));
68+
return implode('|', $sortedAttributes);
69+
}
70+
71+
/**
72+
* Lowercases all array keys and values.
73+
* Performs the lowercasing on a copy (which is returned), to avoid side-effects.
74+
*/
75+
private function caseNormalizeStringArray(array $original): array
76+
{
77+
return unserialize(strtolower(serialize($original)));
78+
}
79+
80+
/**
81+
* Recursively sorts an array via the ksort function. Performs the sort on a copy to avoid side-effects.
82+
*/
83+
private function sortArray(array $sortMe): array
84+
{
85+
$copy = unserialize(serialize($sortMe));
86+
$sortFunction = 'ksort';
87+
$copy = $this->removeEmptyAttributes($copy);
88+
89+
if($this->isSequentialArray($copy)){
90+
$sortFunction = 'sort';
91+
$copy = $this->renumberIndices($copy);
92+
}
93+
94+
$sortFunction($copy);
95+
foreach ($copy as $key => $value) {
96+
if (is_array($value)) {
97+
$sortFunction($value);
98+
$copy[$key] = $this->sortArray($value);
99+
}
100+
}
101+
102+
return $copy;
103+
}
104+
105+
/**
106+
* Determines whether an array is sequential, by checking to see if there's at no string keys in it.
107+
*/
108+
private function isSequentialArray(array $array): bool
109+
{
110+
return count(array_filter(array_keys($array), 'is_string')) === 0;
111+
}
112+
113+
/**
114+
* Reindexes the values of the array so that any skipped numeric indexes are removed.
115+
*/
116+
private function renumberIndices(array $array): array
117+
{
118+
return array_values($array);
119+
}
120+
121+
/**
122+
* Iterate over an array and unset any empty values.
123+
*/
124+
private function removeEmptyAttributes(array $array): array
125+
{
126+
$copy = unserialize(serialize($array));
127+
128+
foreach ($copy as $key => $value) {
129+
if ($this->is_blank($value)) {
130+
unset($copy[$key]);
131+
}
132+
}
133+
134+
return $copy;
135+
}
136+
137+
/**
138+
* Checks if a value is empty, but allowing 0 as an integer, float and string. This means the following are allowed:
139+
* - 0
140+
* - 0.0
141+
* - "0"
142+
* @param $value array|string|integer|float
143+
*/
144+
private function is_blank($value): bool {
145+
return empty($value) && !is_numeric($value);
146+
}
147+
}

src/OpenConext/EngineBlock/Service/ConsentService.php renamed to src/OpenConext/EngineBlock/Service/Consent/ConsentService.php

File renamed without changes.

src/OpenConext/EngineBlock/Service/ConsentServiceInterface.php renamed to src/OpenConext/EngineBlock/Service/Consent/ConsentServiceInterface.php

File renamed without changes.

src/OpenConext/EngineBlockBundle/Resources/config/compat.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ services:
4848
arguments:
4949
- "@engineblock.compat.corto_filter_command_factory"
5050
- "@engineblock.compat.database_connection_factory"
51+
- "@engineblock.service.consent.ConsentHashService"
5152

5253
engineblock.compat.saml2_id_generator:
5354
class: EngineBlock_Saml2_IdGenerator_Default

src/OpenConext/EngineBlockBundle/Resources/config/services.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,3 +345,7 @@ services:
345345
- "@translator"
346346
tags:
347347
- { name: 'twig.extension' }
348+
349+
engineblock.service.consent.ConsentHashService:
350+
class: OpenConext\EngineBlock\Service\Consent\ConsentHashService
351+
public: false

0 commit comments

Comments
 (0)