Skip to content

Commit

Permalink
Allow identity restoration
Browse files Browse the repository at this point in the history
Earlier on it was not possible to restore identities after
they were forgotten. Now logic is added to restore and reset
the identity email, name and tokens in the identity aggregate
so the identity is reset.

#525
  • Loading branch information
pablothedude committed Feb 3, 2025
1 parent ad83840 commit 77bb441
Show file tree
Hide file tree
Showing 11 changed files with 238 additions and 16 deletions.
15 changes: 5 additions & 10 deletions ci/qa/phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1220,11 +1220,6 @@ parameters:
count: 1
path: ../../src/Surfnet/Stepup/Identity/Event/SafeStoreSecretRecoveryTokenPossessionPromisedEvent.php

-
message: "#^Property Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\SafeStoreSecretRecoveryTokenPossessionPromisedEvent\\:\\:\\$secret \\(Surfnet\\\\Stepup\\\\Identity\\\\Value\\\\RecoveryTokenIdentifier\\) does not accept Surfnet\\\\Stepup\\\\Identity\\\\Value\\\\RecoveryTokenIdentifier\\|null\\.$#"
count: 1
path: ../../src/Surfnet/Stepup/Identity/Event/SafeStoreSecretRecoveryTokenPossessionPromisedEvent.php

-
message: "#^Method Surfnet\\\\Stepup\\\\Identity\\\\Event\\\\SecondFactorMigratedEvent\\:\\:deserialize\\(\\) has parameter \\$data with no value type specified in iterable type array\\.$#"
count: 1
Expand Down Expand Up @@ -1412,7 +1407,7 @@ parameters:

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

-
Expand Down Expand Up @@ -1527,7 +1522,7 @@ parameters:

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

-
Expand Down Expand Up @@ -2572,12 +2567,12 @@ parameters:

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

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

-
Expand All @@ -2587,7 +2582,7 @@ parameters:

-
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\\.$#"
count: 3
count: 4
path: ../../src/Surfnet/StepupMiddleware/ApiBundle/Identity/Projector/IdentityProjector.php

-
Expand Down
5 changes: 5 additions & 0 deletions src/Surfnet/Stepup/Identity/Api/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,11 @@ public function expressPreferredLocale(Locale $preferredLocale): void;
*/
public function forget(): void;

public function restore(
CommonName $commonName,
Email $email,
): void;

/**
* @return IdentityId
*/
Expand Down
117 changes: 117 additions & 0 deletions src/Surfnet/Stepup/Identity/Event/IdentityRestoredEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

/**
* Copyright 2025 SURFnet bv
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Surfnet\Stepup\Identity\Event;

use Surfnet\Stepup\Identity\AuditLog\Metadata;
use Surfnet\Stepup\Identity\Value\CommonName;
use Surfnet\Stepup\Identity\Value\Email;
use Surfnet\Stepup\Identity\Value\IdentityId;
use Surfnet\Stepup\Identity\Value\Institution;
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\Forgettable;
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\RightToObtainDataInterface;
use Surfnet\StepupMiddleware\CommandHandlingBundle\SensitiveData\SensitiveData;

class IdentityRestoredEvent extends IdentityEvent implements Forgettable, RightToObtainDataInterface
{

/**
* @var string[]
*/
private array $allowlist = [
'id',
'common_name',
'email',
'institution',
];

public function __construct(
private readonly IdentityId $id,
private readonly Institution $institution,
public CommonName $commonName,
public Email $email,
) {
parent::__construct($id, $institution);
}

public function getAuditLogMetadata(): Metadata
{
$metadata = new Metadata();
$metadata->identityId = $this->id;
$metadata->identityInstitution = $this->institution;

return $metadata;
}

/**
* @param array<string,string> $data
*/
public static function deserialize(array $data): self
{
return new self(
new IdentityId($data['id']),
new Institution($data['institution']),
new CommonName($data['common_name']),
new Email($data['email']),
);
}

/**
* @return array<string,string>
*/
public function serialize(): array
{
return [
'id' => (string)$this->id,
'institution' => (string)$this->institution,
'common_name' => (string)$this->commonName,
'email' => (string)$this->email,
];
}

public function getSensitiveData(): SensitiveData
{
return (new SensitiveData)
->withCommonName($this->commonName)
->withEmail($this->email);
}

public function setSensitiveData(SensitiveData $sensitiveData): void
{
$this->commonName = $sensitiveData->getCommonName();
$this->email = $sensitiveData->getEmail();
}

/**
* @return array<string,string>
*/
public function obtainUserData(): array
{
$serializedPublicUserData = $this->serialize();
$serializedSensitiveUserData = $this->getSensitiveData()->serialize();
return array_merge($serializedPublicUserData, $serializedSensitiveUserData);
}

/**
* @return string[]
*/
public function getAllowlist(): array
{
return $this->allowlist;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,9 +116,14 @@ public function getSensitiveData(): SensitiveData

public function setSensitiveData(SensitiveData $sensitiveData): void
{
$secret = $sensitiveData->getRecoveryTokenIdentifier();
if ($secret === null) {
$secret = SafeStore::unknown();
}

$this->email = $sensitiveData->getEmail();
$this->commonName = $sensitiveData->getCommonName();
$this->secret = $sensitiveData->getRecoveryTokenIdentifier();
$this->secret = $secret;
}

public function obtainUserData(): array
Expand Down
31 changes: 28 additions & 3 deletions src/Surfnet/Stepup/Identity/Identity.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
use Surfnet\Stepup\Identity\Event\IdentityForgottenEvent;
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent;
Expand Down Expand Up @@ -1010,6 +1011,17 @@ public function forget(): void
$this->apply(new IdentityForgottenEvent($this->id, $this->institution));
}

public function restore(
CommonName $commonName,
Email $email,
): void {
if (!$this->forgotten) {
return;
}

$this->apply(new IdentityRestoredEvent($this->id, $this->institution, $commonName, $email));
}

public function allVettedSecondFactorsRemoved(): void
{
$this->apply(
Expand Down Expand Up @@ -1037,6 +1049,19 @@ protected function applyIdentityCreatedEvent(IdentityCreatedEvent $event): void
$this->recoveryTokens = new RecoveryTokenCollection();
}

protected function applyIdentityRestoredEvent(IdentityRestoredEvent $event): void
{
$this->unverifiedSecondFactors = new SecondFactorCollection();
$this->verifiedSecondFactors = new SecondFactorCollection();
$this->vettedSecondFactors = new SecondFactorCollection();
$this->registrationAuthorities = new RegistrationAuthorityCollection();
$this->recoveryTokens = new RecoveryTokenCollection();

$this->commonName = $event->commonName;
$this->email = $event->email;
$this->forgotten = false;
}

public function applyIdentityRenamedEvent(IdentityRenamedEvent $event): void
{
$this->commonName = $event->commonName;
Expand Down Expand Up @@ -1452,9 +1477,9 @@ private function assertNotForgotten(): void
*/
private function assertUserMayAddSecondFactor(int $maxNumberOfTokens): void
{
if (count($this->unverifiedSecondFactors) +
count($this->verifiedSecondFactors) +
count($this->vettedSecondFactors) >= $maxNumberOfTokens
if ($this->unverifiedSecondFactors->count() +
$this->verifiedSecondFactors->count() +
$this->vettedSecondFactors->count() >= $maxNumberOfTokens
) {
throw new DomainException(
sprintf('User may not have more than %d token(s)', $maxNumberOfTokens),
Expand Down
13 changes: 11 additions & 2 deletions src/Surfnet/Stepup/Identity/Value/SafeStore.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,20 @@

namespace Surfnet\Stepup\Identity\Value;

use Surfnet\Stepup\Exception\DomainException;
use Surfnet\Stepup\Exception\RuntimeException;

/**
* Recovery token identifier for the SafeStore token type
*/
class SafeStore implements RecoveryTokenIdentifier
{
private ?Secret $secret = null;

public function __construct(
private readonly Secret $secret,
Secret $secret,
) {
$this->secret = $secret;
}

public static function unknown(): self
Expand All @@ -40,14 +46,17 @@ public static function hidden(): self

public function getValue(): string
{
if ($this->secret === null) {
throw new RuntimeException("Secret should be set");
}
return $this->secret->getSecret();
}

public function equals(RecoveryTokenIdentifier $other): bool
{
return $other instanceof self && $other->getValue() === $this->getValue();
}

public function __toString(): string
{
return $this->getValue();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
use Surfnet\Stepup\Identity\Event\LocalePreferenceExpressedEvent;
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
use Surfnet\Stepup\Identity\Event\RegistrationAuthorityInformationAmendedEvent;
Expand Down Expand Up @@ -293,6 +294,14 @@ public function eventProvider(): array
new Locale('en_GB'),
),
],
'IdentityRestoredEvent' => [
new IdentityRestoredEvent(
new IdentityId($this->UUID()),
new Institution('BabelFish Inc'),
new CommonName('Henk Westbroek'),
new Email('[email protected]'),
),
],
'IdentityEmailChangedEvent' => [
new IdentityEmailChangedEvent(
new IdentityId($this->UUID()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
use Surfnet\Stepup\Identity\Event\IdentityRenamedEvent;
use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenAndVerifiedEvent;
use Surfnet\Stepup\Identity\Event\PhonePossessionProvenEvent;
use Surfnet\Stepup\Identity\Event\PhoneRecoveryTokenPossessionProvenEvent;
Expand Down Expand Up @@ -93,6 +94,7 @@ public function certain_events_are_forgettable_events_and_others_are_not(): void
YubikeyPossessionProvenAndVerifiedEvent::class,
YubikeySecondFactorBootstrappedEvent::class,
RegistrationAuthorityRetractedForInstitutionEvent::class,
IdentityRestoredEvent::class,
];
$otherIdentityEventFqcns = array_diff($this->getConcreteIdentityEventFqcns(), $forgettableEventFqcns);
$forgettableFqcn = Forgettable::class;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

namespace Surfnet\StepupMiddleware\ApiBundle\Identity\Projector;

use Surfnet\Stepup\Identity\Event\IdentityRestoredEvent;
use Surfnet\Stepup\Projector\Projector;
use Surfnet\Stepup\Identity\Event\IdentityCreatedEvent;
use Surfnet\Stepup\Identity\Event\IdentityEmailChangedEvent;
Expand Down Expand Up @@ -75,6 +76,16 @@ public function applyLocalePreferenceExpressedEvent(LocalePreferenceExpressedEve
$this->identityRepository->save($identity);
}

public function applyIdentityRestoredEvent(IdentityRestoredEvent $event): void
{
$identity = $this->identityRepository->find((string)$event->identityId);
$identity->email = $event->email;
$identity->commonName = $event->commonName;

$this->identityRepository->save($identity);
}


public function applySecondFactorVettedEvent(SecondFactorVettedEvent $event): void
{
$this->determinePossessionOfSelfAssertedToken($event->vettingType, (string)$event->identityId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ public function handleUpdateIdentityCommand(UpdateIdentityCommand $command): voi
/** @var IdentityApi $identity */
$identity = $this->eventSourcedRepository->load(new IdentityId($command->id));

$identity->restore(new CommonName($command->commonName), new Email($command->email));
$identity->rename(new CommonName($command->commonName));
$identity->changeEmail(new Email($command->email));

Expand Down
Loading

0 comments on commit 77bb441

Please sign in to comment.