Skip to content

Commit b61e7a6

Browse files
authored
Merge pull request #526 from OpenConext/bugfix/allow-identity-restore
Allow identity restoration
2 parents ad83840 + ea45af0 commit b61e7a6

11 files changed

Lines changed: 237 additions & 15 deletions

File tree

ci/qa/phpstan-baseline.neon

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,11 +1220,6 @@ parameters:
12201220
count: 1
12211221
path: ../../src/Surfnet/Stepup/Identity/Event/SafeStoreSecretRecoveryTokenPossessionPromisedEvent.php
12221222

1223-
-
1224-
message: "#^Property Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\SafeStoreSecretRecoveryTokenPossessionPromisedEvent\\:\\:\\$secret \\(Surfnet\\\\Stepup\\\\Identity\\\\Value\\\\RecoveryTokenIdentifier\\) does not accept Surfnet\\\\Stepup\\\\Identity\\\\Value\\\\RecoveryTokenIdentifier\\|null\\.$#"
1225-
count: 1
1226-
path: ../../src/Surfnet/Stepup/Identity/Event/SafeStoreSecretRecoveryTokenPossessionPromisedEvent.php
1227-
12281223
-
12291224
message: "#^Method Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\SecondFactorMigratedEvent\\:\\:deserialize\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
12301225
count: 1
@@ -1412,7 +1407,7 @@ parameters:
14121407

14131408
-
14141409
message: "#^Cannot call method count\\(\\) on Surfnet\\\\Stepup\\\\Identity\\\\Entity\\\\SecondFactorCollection\\|null\\.$#"
1415-
count: 2
1410+
count: 5
14161411
path: ../../src/Surfnet/Stepup/Identity/Identity.php
14171412

14181413
-
@@ -1527,7 +1522,7 @@ parameters:
15271522

15281523
-
15291524
message: "#^Parameter \\#1 \\$value of function count expects array\\|Countable, Surfnet\\\\Stepup\\\\Identity\\\\Entity\\\\SecondFactorCollection\\|null given\\.$#"
1530-
count: 4
1525+
count: 1
15311526
path: ../../src/Surfnet/Stepup/Identity/Identity.php
15321527

15331528
-
@@ -2572,12 +2567,12 @@ parameters:
25722567

25732568
-
25742569
message: "#^Cannot access property \\$commonName on Surfnet\\\\StepupMiddleware\\\\ApiBundle\\\\Identity\\\\Entity\\\\Identity\\|null\\.$#"
2575-
count: 1
2570+
count: 2
25762571
path: ../../src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/IdentityProjector.php
25772572

25782573
-
25792574
message: "#^Cannot access property \\$email on Surfnet\\\\StepupMiddleware\\\\ApiBundle\\\\Identity\\\\Entity\\\\Identity\\|null\\.$#"
2580-
count: 1
2575+
count: 2
25812576
path: ../../src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/IdentityProjector.php
25822577

25832578
-
@@ -2587,7 +2582,7 @@ parameters:
25872582

25882583
-
25892584
message: "#^Parameter \\#1 \\$identity of method Surfnet\\\\StepupMiddleware\\\\ApiBundle\\\\Identity\\\\Repository\\\\IdentityRepository\\:\\:save\\(\\) expects Surfnet\\\\StepupMiddleware\\\\ApiBundle\\\\Identity\\\\Entity\\\\Identity, Surfnet\\\\StepupMiddleware\\\\ApiBundle\\\\Identity\\\\Entity\\\\Identity\\|null given\\.$#"
2590-
count: 3
2585+
count: 4
25912586
path: ../../src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/IdentityProjector.php
25922587

25932588
-

src/Surfnet/Stepup/Identity/Api/Identity.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,11 @@ public function expressPreferredLocale(Locale $preferredLocale): void;
232232
*/
233233
public function forget(): void;
234234

235+
public function restore(
236+
CommonName $commonName,
237+
Email $email,
238+
): void;
239+
235240
/**
236241
* @return IdentityId
237242
*/
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<?php
2+
3+
/**
4+
* Copyright 2025 SURFnet bv
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 Surfnet\Stepup\Identity\Event;
20+
21+
use Surfnet\Stepup\Identity\AuditLog\Metadata;
22+
use Surfnet\Stepup\Identity\Value\CommonName;
23+
use Surfnet\Stepup\Identity\Value\Email;
24+
use Surfnet\Stepup\Identity\Value\IdentityId;
25+
use Surfnet\Stepup\Identity\Value\Institution;
26+
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\Forgettable;
27+
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\RightToObtainDataInterface;
28+
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\SensitiveData;
29+
30+
class IdentityRestoredEvent extends IdentityEvent implements Forgettable, RightToObtainDataInterface
31+
{
32+
33+
/**
34+
* @var string[]
35+
*/
36+
private array $allowlist = [
37+
'id',
38+
'common_name',
39+
'email',
40+
'institution',
41+
];
42+
43+
public function __construct(
44+
private readonly IdentityId $id,
45+
private readonly Institution $institution,
46+
public CommonName $commonName,
47+
public Email $email,
48+
) {
49+
parent::__construct($id, $institution);
50+
}
51+
52+
public function getAuditLogMetadata(): Metadata
53+
{
54+
$metadata = new Metadata();
55+
$metadata->identityId = $this->id;
56+
$metadata->identityInstitution = $this->institution;
57+
58+
return $metadata;
59+
}
60+
61+
/**
62+
* @param array<string,string> $data
63+
*/
64+
public static function deserialize(array $data): self
65+
{
66+
return new self(
67+
new IdentityId($data['id']),
68+
new Institution($data['institution']),
69+
new CommonName($data['common_name']),
70+
new Email($data['email']),
71+
);
72+
}
73+
74+
/**
75+
* @return array<string,string>
76+
*/
77+
public function serialize(): array
78+
{
79+
return [
80+
'id' => (string)$this->id,
81+
'institution' => (string)$this->institution,
82+
'common_name' => (string)$this->commonName,
83+
'email' => (string)$this->email,
84+
];
85+
}
86+
87+
public function getSensitiveData(): SensitiveData
88+
{
89+
return (new SensitiveData)
90+
->withCommonName($this->commonName)
91+
->withEmail($this->email);
92+
}
93+
94+
public function setSensitiveData(SensitiveData $sensitiveData): void
95+
{
96+
$this->commonName = $sensitiveData->getCommonName();
97+
$this->email = $sensitiveData->getEmail();
98+
}
99+
100+
/**
101+
* @return array<string,string>
102+
*/
103+
public function obtainUserData(): array
104+
{
105+
$serializedPublicUserData = $this->serialize();
106+
$serializedSensitiveUserData = $this->getSensitiveData()->serialize();
107+
return array_merge($serializedPublicUserData, $serializedSensitiveUserData);
108+
}
109+
110+
/**
111+
* @return string[]
112+
*/
113+
public function getAllowlist(): array
114+
{
115+
return $this->allowlist;
116+
}
117+
}

src/Surfnet/Stepup/Identity/Event/SafeStoreSecretRecoveryTokenPossessionPromisedEvent.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,14 @@ public function getSensitiveData(): SensitiveData
116116

117117
public function setSensitiveData(SensitiveData $sensitiveData): void
118118
{
119+
$secret = $sensitiveData->getRecoveryTokenIdentifier();
120+
if ($secret === null) {
121+
$secret = SafeStore::unknown();
122+
}
123+
119124
$this->email = $sensitiveData->getEmail();
120125
$this->commonName = $sensitiveData->getCommonName();
121-
$this->secret = $sensitiveData->getRecoveryTokenIdentifier();
126+
$this->secret = $secret;
122127
}
123128

124129
public function obtainUserData(): array

src/Surfnet/Stepup/Identity/Identity.php

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
5353
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
5454
use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent;
55+
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
5556
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
5657
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
5758
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent;
@@ -1010,6 +1011,17 @@ public function forget(): void
10101011
$this->apply(new IdentityForgottenEvent($this->id, $this->institution));
10111012
}
10121013

1014+
public function restore(
1015+
CommonName $commonName,
1016+
Email $email,
1017+
): void {
1018+
if (!$this->forgotten) {
1019+
return;
1020+
}
1021+
1022+
$this->apply(new IdentityRestoredEvent($this->id, $this->institution, $commonName, $email));
1023+
}
1024+
10131025
public function allVettedSecondFactorsRemoved(): void
10141026
{
10151027
$this->apply(
@@ -1037,6 +1049,19 @@ protected function applyIdentityCreatedEvent(IdentityCreatedEvent $event): void
10371049
$this->recoveryTokens = new RecoveryTokenCollection();
10381050
}
10391051

1052+
protected function applyIdentityRestoredEvent(IdentityRestoredEvent $event): void
1053+
{
1054+
$this->unverifiedSecondFactors = new SecondFactorCollection();
1055+
$this->verifiedSecondFactors = new SecondFactorCollection();
1056+
$this->vettedSecondFactors = new SecondFactorCollection();
1057+
$this->registrationAuthorities = new RegistrationAuthorityCollection();
1058+
$this->recoveryTokens = new RecoveryTokenCollection();
1059+
1060+
$this->commonName = $event->commonName;
1061+
$this->email = $event->email;
1062+
$this->forgotten = false;
1063+
}
1064+
10401065
public function applyIdentityRenamedEvent(IdentityRenamedEvent $event): void
10411066
{
10421067
$this->commonName = $event->commonName;
@@ -1452,9 +1477,9 @@ private function assertNotForgotten(): void
14521477
*/
14531478
private function assertUserMayAddSecondFactor(int $maxNumberOfTokens): void
14541479
{
1455-
if (count($this->unverifiedSecondFactors) +
1456-
count($this->verifiedSecondFactors) +
1457-
count($this->vettedSecondFactors) >= $maxNumberOfTokens
1480+
if ($this->unverifiedSecondFactors->count() +
1481+
$this->verifiedSecondFactors->count() +
1482+
$this->vettedSecondFactors->count() >= $maxNumberOfTokens
14581483
) {
14591484
throw new DomainException(
14601485
sprintf('User may not have more than %d token(s)', $maxNumberOfTokens),

src/Surfnet/Stepup/Identity/Value/SafeStore.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,20 @@
1818

1919
namespace Surfnet\Stepup\Identity\Value;
2020

21+
use Surfnet\Stepup\Exception\DomainException;
22+
use Surfnet\Stepup\Exception\RuntimeException;
23+
2124
/**
2225
* Recovery token identifier for the SafeStore token type
2326
*/
2427
class SafeStore implements RecoveryTokenIdentifier
2528
{
29+
private ?Secret $secret = null;
30+
2631
public function __construct(
27-
private readonly Secret $secret,
32+
Secret $secret,
2833
) {
34+
$this->secret = $secret;
2935
}
3036

3137
public static function unknown(): self
@@ -40,6 +46,9 @@ public static function hidden(): self
4046

4147
public function getValue(): string
4248
{
49+
if ($this->secret === null) {
50+
throw new RuntimeException("Secret should be set");
51+
}
4352
return $this->secret->getSecret();
4453
}
4554

src/Surfnet/Stepup/Tests/Identity/Event/EventSerializationAndDeserializationTest.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
4141
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
4242
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
43+
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
4344
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
4445
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
4546
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedEvent;
@@ -293,6 +294,14 @@ public function eventProvider(): array
293294
new Locale('en_GB'),
294295
),
295296
],
297+
'IdentityRestoredEvent' => [
298+
new IdentityRestoredEvent(
299+
new IdentityId($this->UUID()),
300+
new Institution('BabelFish Inc'),
301+
new CommonName('Henk Westbroek'),
302+
new Email('info@example.invalid'),
303+
),
304+
],
296305
'IdentityEmailChangedEvent' => [
297306
new IdentityEmailChangedEvent(
298307
new IdentityId($this->UUID()),

src/Surfnet/Stepup/Tests/Identity/Event/ForgettableEventsTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
3333
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
3434
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
35+
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
3536
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent;
3637
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
3738
use Surfnet\Stepup\Identity\Event\PhoneRecoveryTokenPossessionProvenEvent;
@@ -93,6 +94,7 @@ public function certain_events_are_forgettable_events_and_others_are_not(): void
9394
YubikeyPossessionProvenAndVerifiedEvent::class,
9495
YubikeySecondFactorBootstrappedEvent::class,
9596
RegistrationAuthorityRetractedForInstitutionEvent::class,
97+
IdentityRestoredEvent::class,
9698
];
9799
$otherIdentityEventFqcns = array_diff($this->getConcreteIdentityEventFqcns(), $forgettableEventFqcns);
98100
$forgettableFqcn = Forgettable::class;

src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/IdentityProjector.php

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

1919
namespace Surfnet\StepupMiddleware\ApiBundle\Identity\Projector;
2020

21+
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
2122
use Surfnet\Stepup\Projector\Projector;
2223
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
2324
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
@@ -75,6 +76,16 @@ public function applyLocalePreferenceExpressedEvent(LocalePreferenceExpressedEve
7576
$this->identityRepository->save($identity);
7677
}
7778

79+
public function applyIdentityRestoredEvent(IdentityRestoredEvent $event): void
80+
{
81+
$identity = $this->identityRepository->find((string)$event->identityId);
82+
$identity->email = $event->email;
83+
$identity->commonName = $event->commonName;
84+
85+
$this->identityRepository->save($identity);
86+
}
87+
88+
7889
public function applySecondFactorVettedEvent(SecondFactorVettedEvent $event): void
7990
{
8091
$this->determinePossessionOfSelfAssertedToken($event->vettingType, (string)$event->identityId);

src/Surfnet/StepupMiddleware/CommandHandlingBundle/Identity/CommandHandler/IdentityCommandHandler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ public function handleUpdateIdentityCommand(UpdateIdentityCommand $command): voi
122122
/** @var IdentityApi $identity */
123123
$identity = $this->eventSourcedRepository->load(new IdentityId($command->id));
124124

125+
$identity->restore(new CommonName($command->commonName), new Email($command->email));
125126
$identity->rename(new CommonName($command->commonName));
126127
$identity->changeEmail(new Email($command->email));
127128

0 commit comments

Comments
 (0)