Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/Application/Import/Ports/ImportHelperInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

namespace SP\Application\Import\Ports;

use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Application\Category\Ports\CategoryService;
use SP\Application\Client\Ports\ClientService;
Expand All @@ -39,6 +40,8 @@ interface ImportHelperInterface
{
public function getAccountService(): AccountService;

public function getAccountPresetService(): AccountPresetService;

/**
* @return CategoryService<CategoryModel>
*/
Expand Down
17 changes: 16 additions & 1 deletion src/Application/Import/Services/ImportBase.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use SP\Application\Application;
use SP\Domain\Crypt\Hash;
use SP\Domain\Account\Dtos\AccountCreateDto;
use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Domain\Category\Models\Category;
use SP\Domain\Category\Models\Category as CategoryModel;
Expand Down Expand Up @@ -68,6 +69,7 @@ abstract class ImportBase extends Service implements ImportService
protected int $version = 0;
protected int $counter = 0;
protected readonly AccountService $accountService;
protected readonly AccountPresetService $accountPresetService;
/** @var CategoryService<CategoryModel> */
protected readonly CategoryService $categoryService;
protected readonly ClientService $clientService;
Expand All @@ -84,6 +86,7 @@ public function __construct(
parent::__construct($application);

$this->accountService = $importHelper->getAccountService();
$this->accountPresetService = $importHelper->getAccountPresetService();
$this->categoryService = $importHelper->getCategoryService();
$this->clientService = $importHelper->getClientService();
$this->tagService = $importHelper->getTagService();
Expand Down Expand Up @@ -157,7 +160,19 @@ final protected function addAccount(
$dto = $dto->mutate(['pass' => $pass, 'key' => '']);
}

$this->accountService->create($dto);
// The password lifetime applies to an imported account too. Every other door that creates
// one — the web form, the API's create and edit-pass — asks the preset service for this;
// the importers went straight to AccountService::create(), which applies the permission
// and privacy presets but has never applied the password ones. So a fixed policy saying
// passwords expire after ninety days said nothing about the several thousand accounts
// that arrived through a CSV.
//
// The lifetime clamp only. checkPasswordPreset(), which validates length and character
// classes, is deliberately not applied here: an import is a migration of credentials that
// already exist elsewhere, and refusing the ones that predate the policy would mean not
// being able to migrate at all. The policy still bites when the account is next edited,
// and the clamp is what makes that happen rather than waiting for someone to notice.
$this->accountService->create($this->accountPresetService->checkPasswordExpiry($dto));
$this->counter++;
}

Expand Down
9 changes: 8 additions & 1 deletion src/Application/Import/Services/ImportHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@

namespace SP\Application\Import\Services;

use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Application\Category\Ports\CategoryService;
use SP\Application\Client\Ports\ClientService;
Expand All @@ -46,7 +47,8 @@ public function __construct(
private CategoryService $categoryService,
private ClientService $clientService,
private TagService $tagService,
private ConfigService $configService
private ConfigService $configService,
private AccountPresetService $accountPresetService
) {
}

Expand All @@ -55,6 +57,11 @@ public function getAccountService(): AccountService
return $this->accountService;
}

public function getAccountPresetService(): AccountPresetService
{
return $this->accountPresetService;
}

/**
* @return CategoryService<CategoryModel>
*/
Expand Down
66 changes: 65 additions & 1 deletion tests/Unit/Application/Import/Services/CsvImportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\Rule\InvokedCount;
use SP\Domain\Account\Dtos\AccountCreateDto;
use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Domain\Category\Models\Category;
use SP\Application\Category\Ports\CategoryService;
Expand Down Expand Up @@ -59,6 +60,9 @@ class CsvImportTest extends UnitaryTestCase
private AccountService|MockObject $accountService;
private MockObject|CategoryService $categoryService;
private ClientService|MockObject $clientService;
private AccountPresetService|MockObject $accountPresetService;
/** @var int|null What the lifetime clamp should answer with, when a test cares. */
private ?int $clampTo = null;
private FileHandlerInterface|MockObject $fileHandler;
private CsvImport $csvImport;

Expand Down Expand Up @@ -139,6 +143,52 @@ public function testDoImport()
$this->csvImport->doImport($params);
}

/**
* An imported account is subject to the password lifetime.
*
* Every other door that creates an account asks the preset service to clamp passDateChange to
* the policy's lifetime — the web form, and the API's create and edit-pass. The importers went
* straight to AccountService::create(), which applies the permission and privacy presets but
* has never applied the password ones, so a fixed policy saying passwords expire after ninety
* days said nothing at all about the several thousand accounts that arrived through a CSV.
*
* The stored account is whatever the clamp returned, not the DTO the importer built.
*
* @throws ImportException
* @throws FileException
*/
public function testAnImportedAccountIsSubjectToThePasswordLifetime()
{
$params = new CsvImportParamsDto($this->fileHandler, 1, 1);

$this->fileHandler
->expects(self::once())
->method('readFromCsv')
->willReturnCallback(
static function () {
yield ['Account_name', 'Client_name', 'Category_name', 'a_url', 'a_login', 'a_password', 'a_note'];
}
);

$this->clientService->expects(self::once())->method('getByName')->willThrowException(NoSuchItemException::error('test'));
$this->clientService->expects(self::once())->method('create')->willReturn(100);
$this->categoryService->expects(self::once())->method('getByName')->willThrowException(NoSuchItemException::error('test'));
$this->categoryService->expects(self::once())->method('create')->willReturn(200);

$clamped = 1_234_567_890;

$this->clampTo = $clamped;

$this->accountService
->expects(self::once())
->method('create')
->with(
new Callback(static fn(AccountCreateDto $dto): bool => $dto->passDateChange === $clamped)
);

$this->csvImport->doImport($params);
}

/**
* @throws ImportException
* @throws FileException
Expand Down Expand Up @@ -396,12 +446,26 @@ protected function setUp(): void
$this->categoryService = $this->createMock(CategoryService::class);
$this->clientService = $this->createMock(ClientService::class);

$this->accountPresetService = $this->createStub(AccountPresetService::class);

// The lifetime clamp runs on import. By default it changes nothing; a test that cares sets
// $clampTo. One stub rather than a per-test override, because the first stub registered is
// the one that answers — a later re-stub of the same method silently does nothing.
$this->accountPresetService
->method('checkPasswordExpiry')
->willReturnCallback(
fn(AccountCreateDto $dto): AccountCreateDto => $this->clampTo === null
? $dto
: $dto->mutate(['passDateChange' => $this->clampTo])
);

$importHelper = new ImportHelper(
$this->accountService,
$this->categoryService,
$this->clientService,
$this->createStub(TagService::class),
$this->createStub(ConfigService::class)
$this->createStub(ConfigService::class),
$this->accountPresetService
);

$crypt = $this->createStub(CryptInterface::class);
Expand Down
12 changes: 10 additions & 2 deletions tests/Unit/Application/Import/Services/KeepassImportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\MockObject\MockObject;
use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Domain\Category\Models\Category;
use SP\Application\Category\Ports\CategoryService;
Expand Down Expand Up @@ -60,6 +61,7 @@ class KeepassImportTest extends UnitaryTestCase
private AccountService|MockObject $accountService;
private MockObject|CategoryService $categoryService;
private ClientService|MockObject $clientService;
private AccountPresetService|MockObject $accountPresetService;

/**
* @throws Exception
Expand Down Expand Up @@ -195,7 +197,8 @@ public function testDoImportWithEmptyGroupName()
$this->categoryService,
$this->clientService,
$this->createStub(TagService::class),
$this->createStub(ConfigService::class)
$this->createStub(ConfigService::class),
$this->accountPresetService
);

$keepassImport = new KeepassImport(
Expand All @@ -216,12 +219,17 @@ protected function setUp(): void
$this->categoryService = $this->createMock(CategoryService::class);
$this->clientService = $this->createMock(ClientService::class);

$this->accountPresetService = $this->createStub(AccountPresetService::class);
// The lifetime clamp runs on import; by default it changes nothing.
$this->accountPresetService->method('checkPasswordExpiry')->willReturnArgument(0);

$importHelper = new ImportHelper(
$this->accountService,
$this->categoryService,
$this->clientService,
$this->createStub(TagService::class),
$this->createStub(ConfigService::class)
$this->createStub(ConfigService::class),
$this->accountPresetService
);

$crypt = $this->createStub(CryptInterface::class);
Expand Down
36 changes: 26 additions & 10 deletions tests/Unit/Application/Import/Services/SyspassImportTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use RuntimeException;
use SP\Infrastructure\Crypt\Crypt;
use SP\Domain\Account\Dtos\AccountCreateDto;
use SP\Application\Account\Ports\AccountPresetService;
use SP\Application\Account\Ports\AccountService;
use SP\Domain\Category\Models\Category;
use SP\Application\Category\Ports\CategoryService;
Expand Down Expand Up @@ -74,6 +75,7 @@ class SyspassImportTest extends UnitaryTestCase
private CryptInterface|MockObject $crypt;
private SyspassImport $sysPassImport;
private ConfigService|MockObject $configService;
private AccountPresetService|MockObject $accountPresetService;

/**
* @throws ImportException
Expand Down Expand Up @@ -594,7 +596,8 @@ public function testDoImportWithEncryptedFile()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -679,7 +682,8 @@ public function testDoImportWithEncryptedFileAndCryptoException()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -747,7 +751,8 @@ public function testDoImportRefusesEncryptedFileWithNoPasswordSupplied()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -789,7 +794,8 @@ public function testDoImportDecryptsRawDataForModernFormatWithoutBase64Decoding(
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -841,7 +847,8 @@ public function testDoImportRejectsFilesEncryptedByOldSysPassVersions()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -891,7 +898,8 @@ public function testDoImportTreatsUndecodableDecryptedDataAsWrongPassword()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -951,7 +959,8 @@ public function testDoImportAbortsWhenAccountReferencesUnknownCategory()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -1001,7 +1010,8 @@ public function testDoImportAbortsWhenAccountReferencesUnknownClient()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -1067,7 +1077,8 @@ public function testDoImportRejectsEncryptedAccountsFromOldSysPassVersions()
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$document = new DOMDocument();
Expand Down Expand Up @@ -1229,13 +1240,18 @@ protected function setUp(): void
$this->clientService = $this->createMock(ClientService::class);
$this->tagService = $this->createMock(TagService::class);
$this->configService = $this->createMock(ConfigService::class);
$this->accountPresetService = $this->createStub(AccountPresetService::class);
// The lifetime clamp is applied on import; by default it changes nothing.
$this->accountPresetService->method('checkPasswordExpiry')
->willReturnArgument(0);

$importHelper = new ImportHelper(
$this->accountService,
$this->categoryService,
$this->clientService,
$this->tagService,
$this->configService
$this->configService,
$this->accountPresetService
);

$this->crypt = $this->createMock(CryptInterface::class);
Expand Down