diff --git a/lib/SP/DataModel/EncryptedModel.php b/lib/SP/DataModel/EncryptedModel.php index 3df0d768..77fc3723 100644 --- a/lib/SP/DataModel/EncryptedModel.php +++ b/lib/SP/DataModel/EncryptedModel.php @@ -56,11 +56,13 @@ trait EncryptedModel $data = $this->{$instance->getDataProperty()}; if ($data !== null) { + $key = $crypt->makeSecuredKey($password); + return $this->mutate([ - $instance->getKeyProperty() => $crypt->makeSecuredKey($password), + $instance->getKeyProperty() => $key, $instance->getDataProperty() => $crypt->encrypt( $data, - $this->{$instance->getKeyProperty()}, + $key, $password ) ]); @@ -88,12 +90,13 @@ trait EncryptedModel $instance = $attribute->newInstance(); $data = $this->{$instance->getDataProperty()}; + $key = $this->{$instance->getKeyProperty()}; - if ($data !== null) { + if ($data !== null && $key !== null) { return $this->mutate([ $instance->getDataProperty() => $crypt->decrypt( $data, - $this->{$instance->getKeyProperty()}, + $key, $password ) ]); diff --git a/lib/SP/Domain/Plugin/Ports/PluginDataService.php b/lib/SP/Domain/Plugin/Ports/PluginDataService.php index eb22dd19..a074ebbe 100644 --- a/lib/SP/Domain/Plugin/Ports/PluginDataService.php +++ b/lib/SP/Domain/Plugin/Ports/PluginDataService.php @@ -49,7 +49,7 @@ interface PluginDataService * @throws QueryException * @throws ServiceException */ - public function create(PluginDataModel $itemData): QueryResult; + public function create(PluginDataModel $pluginData): QueryResult; /** * Updates an item @@ -60,7 +60,7 @@ interface PluginDataService * @throws QueryException * @throws ServiceException */ - public function update(PluginDataModel $itemData): int; + public function update(PluginDataModel $pluginData): int; /** * Returns the item for given plugin and id @@ -77,7 +77,7 @@ interface PluginDataService /** * Returns the item for given id * - * @return PluginDataModel[] + * @return array * @throws CryptoException * @throws ConstraintException * @throws NoSuchPropertyException @@ -85,12 +85,12 @@ interface PluginDataService * @throws NoSuchItemException * @throws ServiceException */ - public function getById(string $id): array; + public function getByName(string $name): array; /** * Returns all the items * - * @return PluginDataModel[] + * @return array * @throws CryptoException * @throws ConstraintException * @throws NoSuchPropertyException @@ -106,7 +106,7 @@ interface PluginDataService * @throws QueryException * @throws NoSuchItemException */ - public function delete(string $id): void; + public function delete(string $name): void; /** * Deletes an item diff --git a/lib/SP/Domain/Plugin/Ports/PluginLoaderInterface.php b/lib/SP/Domain/Plugin/Ports/PluginLoaderService.php similarity index 90% rename from lib/SP/Domain/Plugin/Ports/PluginLoaderInterface.php rename to lib/SP/Domain/Plugin/Ports/PluginLoaderService.php index 82c93c67..6ca9140a 100644 --- a/lib/SP/Domain/Plugin/Ports/PluginLoaderInterface.php +++ b/lib/SP/Domain/Plugin/Ports/PluginLoaderService.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2024, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -27,7 +27,7 @@ namespace SP\Domain\Plugin\Ports; /** * Interface PluginLoaderInterface */ -interface PluginLoaderInterface +interface PluginLoaderService { public function loadFor(PluginInterface $plugin): void; } diff --git a/lib/SP/Domain/Plugin/Services/PluginData.php b/lib/SP/Domain/Plugin/Services/PluginData.php index c9df8929..232cafff 100644 --- a/lib/SP/Domain/Plugin/Services/PluginData.php +++ b/lib/SP/Domain/Plugin/Services/PluginData.php @@ -32,6 +32,7 @@ use SP\Domain\Core\Exceptions\ConstraintException; use SP\Domain\Core\Exceptions\CryptException; use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Core\Exceptions\SPException; +use SP\Domain\Plugin\Models\PluginData as PluginDataModel; use SP\Domain\Plugin\Ports\PluginDataRepository; use SP\Domain\Plugin\Ports\PluginDataService; use SP\Infrastructure\Common\Repositories\NoSuchItemException; @@ -41,6 +42,8 @@ use function SP\__u; /** * Class PluginData + * + * @template T of PluginDataModel */ final class PluginData extends Service implements PluginDataService { @@ -56,87 +59,89 @@ final class PluginData extends Service implements PluginDataService /** * Creates an item * - * @param \SP\Domain\Plugin\Models\PluginData $itemData + * @param PluginDataModel $pluginData * @return QueryResult * @throws ConstraintException * @throws CryptException * @throws QueryException * @throws ServiceException */ - public function create(PluginData $itemData): QueryResult + public function create(PluginDataModel $pluginData): QueryResult { - return $this->pluginDataRepository->create($itemData->encrypt($this->getMasterKeyFromContext(), $this->crypt)); + return $this->pluginDataRepository->create( + $pluginData->encrypt($this->getMasterKeyFromContext(), $this->crypt) + ); } /** * Updates an item * - * @param \SP\Domain\Plugin\Models\PluginData $itemData + * @param PluginDataModel $pluginData * @return int * @throws ConstraintException * @throws CryptException * @throws QueryException * @throws ServiceException */ - public function update(PluginData $itemData): int + public function update(PluginDataModel $pluginData): int { - return $this->pluginDataRepository->update($itemData->encrypt($this->getMasterKeyFromContext(), $this->crypt)); + return $this->pluginDataRepository->update( + $pluginData->encrypt($this->getMasterKeyFromContext(), $this->crypt) + ); } /** * Returns the item for given plugin and id * * @param string $name - * @param int $id - * @return PluginData + * @param int $itemId + * @return PluginDataModel * @throws ConstraintException * @throws CryptException * @throws NoSuchItemException * @throws QueryException * @throws ServiceException */ - public function getByItemId(string $name, int $id): PluginData + public function getByItemId(string $name, int $itemId): PluginDataModel { - $result = $this->pluginDataRepository->getByItemId($name, $id); + $result = $this->pluginDataRepository->getByItemId($name, $itemId); if ($result->getNumRows() === 0) { - throw new NoSuchItemException(__u('Plugin\'s data not found'), SPException::INFO); + throw NoSuchItemException::info(__u('Plugin\'s data not found')); } - return $result->getData(PluginData::class) + return $result->getData(PluginDataModel::class) ->decrypt($this->getMasterKeyFromContext(), $this->crypt); } /** * Returns the item for given id * - * @param string $id - * @return PluginData[] - * @throws ConstraintException + * @param string $name + * @return array * @throws CryptException * @throws NoSuchItemException - * @throws QueryException * @throws ServiceException */ - public function getById(string $id): array + public function getByName(string $name): array { - $result = $this->pluginDataRepository->getByName($id); + $result = $this->pluginDataRepository->getByName($name); if ($result->getNumRows() === 0) { - throw new NoSuchItemException(__u('Plugin\'s data not found'), SPException::INFO); + throw NoSuchItemException::info(__u('Plugin\'s data not found')); } return array_map( - fn(PluginData $itemData) => $itemData->decrypt($this->getMasterKeyFromContext(), $this->crypt), - $result->getDataAsArray() + fn(PluginDataModel $pluginData) => $pluginData->decrypt($this->getMasterKeyFromContext(), $this->crypt), + $result->getDataAsArray(PluginDataModel::class) ); } /** * Returns all the items * - * @return PluginData[] + * @return array * @throws ConstraintException * @throws CryptException * @throws QueryException @@ -146,8 +151,8 @@ final class PluginData extends Service implements PluginDataService public function getAll(): array { return array_map( - fn(PluginData $itemData) => $itemData->decrypt($this->getMasterKeyFromContext(), $this->crypt), - $this->pluginDataRepository->getAll()->getDataAsArray() + fn(PluginDataModel $pluginData) => $pluginData->decrypt($this->getMasterKeyFromContext(), $this->crypt), + $this->pluginDataRepository->getAll()->getDataAsArray(PluginDataModel::class) ); } @@ -158,10 +163,10 @@ final class PluginData extends Service implements PluginDataService * @throws QueryException * @throws NoSuchItemException */ - public function delete(string $id): void + public function delete(string $name): void { - if ($this->pluginDataRepository->delete($id) === 0) { - throw new NoSuchItemException(__u('Plugin\'s data not found'), SPException::INFO); + if ($this->pluginDataRepository->delete($name)->getAffectedNumRows() === 0) { + throw NoSuchItemException::info(__u('Plugin\'s data not found')); } } @@ -174,8 +179,8 @@ final class PluginData extends Service implements PluginDataService */ public function deleteByItemId(string $name, int $itemId): void { - if ($this->pluginDataRepository->deleteByItemId($name, $itemId) === 0) { - throw new NoSuchItemException(__u('Plugin\'s data not found'), SPException::INFO); + if ($this->pluginDataRepository->deleteByItemId($name, $itemId)->getAffectedNumRows() === 0) { + throw NoSuchItemException::info(__u('Plugin\'s data not found')); } } } diff --git a/lib/SP/Domain/Plugin/Services/PluginLoader.php b/lib/SP/Domain/Plugin/Services/PluginLoader.php index 3e158495..aa0cdc58 100644 --- a/lib/SP/Domain/Plugin/Services/PluginLoader.php +++ b/lib/SP/Domain/Plugin/Services/PluginLoader.php @@ -31,7 +31,7 @@ use SP\Domain\Common\Services\Service; use SP\Domain\Core\Exceptions\ConstraintException; use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Plugin\Ports\PluginInterface; -use SP\Domain\Plugin\Ports\PluginLoaderInterface; +use SP\Domain\Plugin\Ports\PluginLoaderService; use SP\Domain\Plugin\Ports\PluginManagerService; use SP\Infrastructure\Common\Repositories\NoSuchItemException; @@ -40,7 +40,7 @@ use function SP\__; /** * Class PluginLoader */ -final class PluginLoader extends Service implements PluginLoaderInterface +final class PluginLoader extends Service implements PluginLoaderService { public function __construct(Application $application, private readonly PluginManagerService $pluginService) { diff --git a/lib/SP/Infrastructure/Plugin/Repositories/PluginData.php b/lib/SP/Infrastructure/Plugin/Repositories/PluginData.php index 86f996d4..5283ea83 100644 --- a/lib/SP/Infrastructure/Plugin/Repositories/PluginData.php +++ b/lib/SP/Infrastructure/Plugin/Repositories/PluginData.php @@ -151,8 +151,7 @@ final class PluginData extends BaseRepository implements PluginDataRepository ->from(self::TABLE) ->cols(PluginDataModel::getCols()) ->where('name = :name') - ->bindValues(['name' => $name]) - ->limit(1); + ->bindValues(['name' => $name]); $queryData = QueryData::buildWithMapper($query, PluginDataModel::class); diff --git a/lib/SP/Plugin/PluginBase.php b/lib/SP/Plugin/PluginBase.php index 40e67efc..c211a90e 100644 --- a/lib/SP/Plugin/PluginBase.php +++ b/lib/SP/Plugin/PluginBase.php @@ -31,7 +31,7 @@ use SP\Domain\Core\Exceptions\NoSuchPropertyException; use SP\Domain\Core\Exceptions\QueryException; use SP\Domain\Plugin\Ports\PluginCompatilityService; use SP\Domain\Plugin\Ports\PluginInterface; -use SP\Domain\Plugin\Ports\PluginLoaderInterface; +use SP\Domain\Plugin\Ports\PluginLoaderService; use SP\Domain\Plugin\Ports\PluginOperationInterface; use SP\Infrastructure\Common\Repositories\NoSuchItemException; @@ -52,7 +52,7 @@ abstract class PluginBase implements PluginInterface public function __construct( protected readonly PluginOperationInterface $pluginOperation, private readonly PluginCompatilityService $pluginCompatilityService, - private readonly PluginLoaderInterface $pluginLoadService + private readonly PluginLoaderService $pluginLoadService ) { $this->load(); } diff --git a/tests/SPT/Domain/Plugin/Services/PluginDataTest.php b/tests/SPT/Domain/Plugin/Services/PluginDataTest.php new file mode 100644 index 00000000..4ba04005 --- /dev/null +++ b/tests/SPT/Domain/Plugin/Services/PluginDataTest.php @@ -0,0 +1,359 @@ +. + */ + +namespace SPT\Domain\Plugin\Services; + +use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\MockObject\MockObject; +use SP\Core\Context\ContextException; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Core\Context\ContextInterface; +use SP\Domain\Core\Crypt\CryptInterface; +use SP\Domain\Core\Exceptions\ConstraintException; +use SP\Domain\Core\Exceptions\CryptException; +use SP\Domain\Core\Exceptions\QueryException; +use SP\Domain\Core\Exceptions\SPException; +use SP\Domain\Plugin\Models\PluginData as PluginDataModel; +use SP\Domain\Plugin\Ports\PluginDataRepository; +use SP\Domain\Plugin\Services\PluginData; +use SP\Infrastructure\Common\Repositories\NoSuchItemException; +use SP\Infrastructure\Database\QueryResult; +use SPT\Generators\PluginDataGenerator; +use SPT\UnitaryTestCase; + +/** + * Class PluginDataTest + */ +#[Group('unitary')] +class PluginDataTest extends UnitaryTestCase +{ + + private PluginData $pluginData; + private PluginDataRepository|MockObject $pluginDataRepository; + private CryptInterface|MockObject $crypt; + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws QueryException + */ + public function testDelete() + { + $queryResult = new QueryResult(); + $this->pluginDataRepository + ->expects($this->once()) + ->method('delete') + ->with('test_plugin') + ->willReturn($queryResult->setAffectedNumRows(1)); + + $this->pluginData->delete('test_plugin'); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws QueryException + */ + public function testDeleteWithException() + { + $queryResult = new QueryResult(); + $this->pluginDataRepository + ->expects($this->once()) + ->method('delete') + ->with('test_plugin') + ->willReturn($queryResult->setAffectedNumRows(0)); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Plugin\'s data not found'); + + $this->pluginData->delete('test_plugin'); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws QueryException + */ + public function testDeleteByItemId() + { + $queryResult = new QueryResult(); + $this->pluginDataRepository + ->expects($this->once()) + ->method('deleteByItemId') + ->with('test_plugin', 100) + ->willReturn($queryResult->setAffectedNumRows(1)); + + $this->pluginData->deleteByItemId('test_plugin', 100); + } + + /** + * @throws ConstraintException + * @throws NoSuchItemException + * @throws QueryException + */ + public function testDeleteByItemIdWithException() + { + $queryResult = new QueryResult(); + $this->pluginDataRepository + ->expects($this->once()) + ->method('deleteByItemId') + ->with('test_plugin', 100) + ->willReturn($queryResult->setAffectedNumRows(0)); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Plugin\'s data not found'); + + $this->pluginData->deleteByItemId('test_plugin', 100); + } + + /** + * @throws ServiceException + * @throws ConstraintException + * @throws CryptException + * @throws QueryException + * @throws ContextException + */ + public function testUpdate() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'super_secret'); + + $pluginData = PluginDataGenerator::factory()->buildPluginData(); + + $this->crypt + ->expects($this->once()) + ->method('encrypt') + ->with($pluginData->getData(), self::anything(), 'super_secret') + ->willReturn('encrypt_data'); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('update') + ->with( + self::callback(function (PluginDataModel $pluginDataModel) use ($pluginData) { + return $pluginDataModel->getData() !== $pluginData->getData() + && $pluginDataModel->getKey() !== $pluginData->getKey() + && $pluginDataModel->getName() === $pluginData->getName() + && $pluginDataModel->getItemId() === $pluginData->getItemId(); + }) + ) + ->willReturn(1); + + $this->pluginData->update($pluginData); + } + + /** + * @throws ServiceException + * @throws ContextException + * @throws ConstraintException + * @throws CryptException + * @throws QueryException + */ + public function testCreate() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'super_secret'); + + $pluginData = PluginDataGenerator::factory()->buildPluginData(); + + $this->crypt + ->expects($this->once()) + ->method('encrypt') + ->with($pluginData->getData(), self::anything(), 'super_secret') + ->willReturn('encrypt_data'); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('create') + ->with( + self::callback(function (PluginDataModel $pluginDataModel) use ($pluginData) { + return $pluginDataModel->getData() !== $pluginData->getData() + && $pluginDataModel->getKey() !== $pluginData->getKey() + && $pluginDataModel->getName() === $pluginData->getName() + && $pluginDataModel->getItemId() === $pluginData->getItemId(); + }) + ) + ->willReturn(new QueryResult()); + + $this->pluginData->create($pluginData); + } + + /** + * @throws NoSuchItemException + * @throws ServiceException + * @throws ContextException + * @throws ConstraintException + * @throws CryptException + * @throws QueryException + */ + public function testGetByItemId() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'super_secret'); + + $pluginData = PluginDataGenerator::factory()->buildPluginData(); + + $queryResult = new QueryResult([$pluginData]); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('getByItemId') + ->with('test_plugin', 100) + ->willReturn($queryResult); + + $this->crypt + ->expects($this->once()) + ->method('decrypt') + ->with($pluginData->getData(), $pluginData->getKey(), 'super_secret') + ->willReturn('plain_data'); + + $out = $this->pluginData->getByItemId('test_plugin', 100); + + $this->assertEquals('plain_data', $out->getData()); + } + + /** + * @throws ConstraintException + * @throws CryptException + * @throws NoSuchItemException + * @throws QueryException + * @throws ServiceException + */ + public function testGetByItemIdWithNoRows() + { + $this->pluginDataRepository + ->expects($this->once()) + ->method('getByItemId') + ->with('test_plugin', 100) + ->willReturn(new QueryResult([])); + + $this->crypt + ->expects($this->never()) + ->method('decrypt'); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Plugin\'s data not found'); + + $this->pluginData->getByItemId('test_plugin', 100); + } + + /** + * @throws NoSuchItemException + * @throws ContextException + * @throws ServiceException + * @throws CryptException + */ + public function testGetByName() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'super_secret'); + + $pluginData = PluginDataGenerator::factory()->buildPluginData(); + + $queryResult = new QueryResult([$pluginData, $pluginData]); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('getByName') + ->with('test_plugin') + ->willReturn($queryResult); + + $this->crypt + ->expects($this->exactly(2)) + ->method('decrypt') + ->with($pluginData->getData(), $pluginData->getKey(), 'super_secret') + ->willReturn('plain_data'); + + $out = $this->pluginData->getByName('test_plugin'); + + $this->assertCount(2, $out); + $this->assertEquals('plain_data', $out[0]->getData()); + $this->assertEquals('plain_data', $out[1]->getData()); + } + + /** + * @throws CryptException + * @throws NoSuchItemException + * @throws ServiceException + */ + public function testGetByNameWithNoRows() + { + $queryResult = new QueryResult([]); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('getByName') + ->with('test_plugin') + ->willReturn($queryResult); + + $this->crypt + ->expects($this->never()) + ->method('decrypt'); + + $this->expectException(NoSuchItemException::class); + $this->expectExceptionMessage('Plugin\'s data not found'); + + $this->pluginData->getByName('test_plugin'); + } + + /** + * @throws ServiceException + * @throws ContextException + * @throws ConstraintException + * @throws CryptException + * @throws SPException + * @throws QueryException + */ + public function testGetAll() + { + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, 'super_secret'); + + $pluginData = PluginDataGenerator::factory()->buildPluginData(); + + $queryResult = new QueryResult([$pluginData, $pluginData]); + + $this->pluginDataRepository + ->expects($this->once()) + ->method('getAll') + ->willReturn($queryResult); + + $this->crypt + ->expects($this->exactly(2)) + ->method('decrypt') + ->with($pluginData->getData(), $pluginData->getKey(), 'super_secret') + ->willReturn('plain_data'); + + $out = $this->pluginData->getAll(); + + $this->assertCount(2, $out); + $this->assertEquals('plain_data', $out[0]->getData()); + $this->assertEquals('plain_data', $out[1]->getData()); + } + + protected function setUp(): void + { + parent::setUp(); + + $this->pluginDataRepository = $this->createMock(PluginDataRepository::class); + $this->crypt = $this->createMock(CryptInterface::class); + + $this->pluginData = new PluginData($this->application, $this->pluginDataRepository, $this->crypt); + } +}