diff --git a/app/modules/api/Controllers/Help/AccountHelp.php b/app/modules/api/Controllers/Help/AccountHelp.php index 3ecb6eb7..322e3a7f 100644 --- a/app/modules/api/Controllers/Help/AccountHelp.php +++ b/app/modules/api/Controllers/Help/AccountHelp.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,6 +24,8 @@ namespace SP\Modules\Api\Controllers\Help; +use function SP\__; + /** * Class AccountHelp * @@ -146,4 +148,4 @@ final class AccountHelp implements HelpInterface self::getItem('id', __('Account Id'), true) ]; } -} \ No newline at end of file +} diff --git a/app/modules/api/Controllers/Help/HelpTrait.php b/app/modules/api/Controllers/Help/HelpTrait.php index 4bb3ac07..cf547fd0 100644 --- a/app/modules/api/Controllers/Help/HelpTrait.php +++ b/app/modules/api/Controllers/Help/HelpTrait.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -32,19 +32,19 @@ namespace SP\Modules\Api\Controllers\Help; trait HelpTrait { /** - * @param string $action + * @param string $action * * @return array */ public static function getHelpFor(string $action): array { - if (strpos($action, '/') !== false) { + if (str_contains($action, '/')) { [, $action] = explode('/', $action); } if (method_exists(static::class, $action)) { return [ - 'help' => static::$action() + 'help' => static::$action(), ]; } @@ -52,19 +52,19 @@ trait HelpTrait } /** - * @param string $name - * @param string $description - * @param bool $required + * @param string $name + * @param string $description + * @param bool $required * * @return array */ private static function getItem( string $name, string $description, - bool $required = false): array - { + bool $required = false + ): array { return [ - $name => ['description' => $description, 'required' => $required] + $name => ['description' => $description, 'required' => $required], ]; } -} \ No newline at end of file +} diff --git a/app/modules/api/module.php b/app/modules/api/module.php index 1c3d04c4..39d83373 100644 --- a/app/modules/api/module.php +++ b/app/modules/api/module.php @@ -22,6 +22,7 @@ * along with sysPass. If not, see . */ +use SP\Domain\Api\Ports\ApiRequestInterface; use SP\Domain\Api\Services\ApiRequest; use function DI\factory; @@ -29,5 +30,5 @@ const MODULE_PATH = __DIR__; const PLUGINS_PATH = MODULE_PATH.DIRECTORY_SEPARATOR.'plugins'; return [ - ApiRequest::class => factory([ApiRequest::class, 'buildFromRequest']), + ApiRequestInterface::class => factory([ApiRequest::class, 'buildFromRequest']), ]; diff --git a/lib/SP/Core/Bootstrap/BootstrapApi.php b/lib/SP/Core/Bootstrap/BootstrapApi.php index 4d209f48..ccf39492 100644 --- a/lib/SP/Core/Bootstrap/BootstrapApi.php +++ b/lib/SP/Core/Bootstrap/BootstrapApi.php @@ -31,7 +31,7 @@ use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; use SP\Core\HttpModuleBase; -use SP\Domain\Api\Services\ApiRequest; +use SP\Domain\Api\Ports\ApiRequestInterface; use SP\Domain\Api\Services\JsonRpcResponse; use SP\Modules\Api\Init as InitApi; use function SP\logger; @@ -78,7 +78,7 @@ final class BootstrapApi extends BootstrapBase try { logger('API route'); - $apiRequest = $this->createObjectFor(ApiRequest::class); + $apiRequest = $this->createObjectFor(ApiRequestInterface::class); [$controllerName, $actionName] = explode('/', $apiRequest->getMethod()); diff --git a/lib/SP/Core/Crypt/Hash.php b/lib/SP/Core/Crypt/Hash.php index aa9ed439..1f5a7b49 100644 --- a/lib/SP/Core/Crypt/Hash.php +++ b/lib/SP/Core/Crypt/Hash.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,6 +24,8 @@ namespace SP\Core\Crypt; +use function SP\logger; + /** * Class Hash * @@ -34,14 +36,14 @@ final class Hash /** * Longitud máxima aceptada para hashing */ - public const MAX_KEY_LENGTH = 72; - private const HASH_ALGO = 'sha256'; + public const MAX_KEY_LENGTH = 72; + private const HASH_ALGO = 'sha256'; /** * Comprobar el hash de una clave. * - * @param string $key con la clave a comprobar - * @param string $hash con el hash a comprobar + * @param string $key con la clave a comprobar + * @param string $hash con el hash a comprobar */ public static function checkHashKey(string $key, string $hash): bool { @@ -51,8 +53,8 @@ final class Hash /** * Devolver la clave preparada. Se crea un hash si supera la longitud máxima. * - * @param string $key - * @param bool $isCheck Indica si la operación es de comprobación o no + * @param string $key + * @param bool $isCheck Indica si la operación es de comprobación o no * * @return string */ @@ -72,7 +74,7 @@ final class Hash /** * Generar un hash de una clave criptográficamente segura * - * @param string $key con la clave a 'hashear' + * @param string $key con la clave a 'hashear' * * @return string con el hash de la clave */ @@ -88,8 +90,7 @@ final class Hash string $message, string $key, string $hash - ): bool - { + ): bool { return hash_equals($hash, self::signMessage($message, $key)); } @@ -100,4 +101,4 @@ final class Hash { return hash_hmac(self::HASH_ALGO, $message, $key); } -} \ No newline at end of file +} diff --git a/lib/SP/DataModel/AuthTokenData.php b/lib/SP/DataModel/AuthTokenData.php index f28460ac..655f4fd5 100644 --- a/lib/SP/DataModel/AuthTokenData.php +++ b/lib/SP/DataModel/AuthTokenData.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,7 +24,6 @@ namespace SP\DataModel; -use SP\Core\Crypt\Vault; use SP\Domain\Common\Adapters\DataModelInterface; use SP\Domain\Common\Models\Model; @@ -35,172 +34,57 @@ use SP\Domain\Common\Models\Model; */ class AuthTokenData extends Model implements DataModelInterface { - /** - * @var int - */ - public $id; - /** - * @var string - */ - public $vault; - /** - * @var int - */ - public $userId; - /** - * @var string - */ - public $token = ''; - /** - * @var int - */ - public $createdBy; - /** - * @var int - */ - public $startDate; - /** - * @var int - */ - public $actionId; - /** - * @var string - */ - public $hash; + public ?int $userId = null; + public ?string $token = null; + public ?int $createdBy = null; + public ?int $startDate = null; + public ?int $actionId = null; + public ?string $hash = null; + protected ?int $id = null; + protected ?string $vault = null; - /** - * @return int - */ - public function getId() + public function getId(): ?int { return (int)$this->id; } - /** - * @param int $id - */ - public function setId($id) - { - $this->id = (int)$id; - } - - /** - * @return string - */ - public function getVault() + public function getVault(): ?string { return $this->vault; } - /** - * @param Vault $vault - */ - public function setVault(Vault $vault) + public function getUserId(): ?int { - $this->vault = serialize($vault); + return $this->userId; } - /** - * @return int - */ - public function getUserId() - { - return (int)$this->userId; - } - - /** - * @param int $userId - */ - public function setUserId($userId) - { - $this->userId = (int)$userId; - } - - /** - * @return string - */ - public function getToken() + public function getToken(): ?string { return $this->token; } - /** - * @param string $token - */ - public function setToken($token) + public function getCreatedBy(): ?int { - $this->token = $token; + return $this->createdBy; } - /** - * @return int - */ - public function getCreatedBy() + public function getStartDate(): ?int { - return (int)$this->createdBy; + return $this->startDate; } - /** - * @param int $createdBy - */ - public function setCreatedBy($createdBy) + public function getName(): ?string { - $this->createdBy = (int)$createdBy; + return null; } - /** - * @return int - */ - public function getStartDate() + public function getActionId(): ?int { - return (int)$this->startDate; + return $this->actionId; } - /** - * @param int $startDate - */ - public function setStartDate($startDate) - { - $this->startDate = (int)$startDate; - } - - /** - * @return string - */ - public function getName() - { - return ''; - } - - /** - * @return int - */ - public function getActionId() - { - return (int)$this->actionId; - } - - /** - * @param int $actionId - */ - public function setActionId($actionId) - { - $this->actionId = (int)$actionId; - } - - /** - * @return string - */ - public function getHash() + public function getHash(): ?string { return $this->hash; } - - /** - * @param string $hash - */ - public function setHash($hash) - { - $this->hash = $hash; - } } diff --git a/lib/SP/DataModel/ProfileData.php b/lib/SP/DataModel/ProfileData.php index 482c9806..782850ef 100644 --- a/lib/SP/DataModel/ProfileData.php +++ b/lib/SP/DataModel/ProfileData.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,14 +24,14 @@ namespace SP\DataModel; -defined('APP_ROOT') || die(); +use SP\Domain\Common\Models\Model; /** * Class ProfileData * * @package SP\DataModel */ -class ProfileData +class ProfileData extends Model { protected bool $accView = false; protected bool $accViewPass = false; diff --git a/lib/SP/DataModel/UserData.php b/lib/SP/DataModel/UserData.php index 73adb8d5..2b4139c7 100644 --- a/lib/SP/DataModel/UserData.php +++ b/lib/SP/DataModel/UserData.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -65,10 +65,7 @@ class UserData extends UserPassData implements DataModelInterface return $this->lastLogin; } - /** - * @return string - */ - public function getLastUpdate() + public function getLastUpdate(): ?string { return $this->lastUpdate; } @@ -88,111 +85,56 @@ class UserData extends UserPassData implements DataModelInterface return $this->email; } - public function setEmail(string $email) - { - $this->email = $email; - } - public function getNotes(): ?string { return $this->notes; } - public function setNotes(string $notes) - { - $this->notes = $notes; - } - public function getUserGroupId(): int { return (int)$this->userGroupId; } - public function setUserGroupId(int $userGroupId) - { - $this->userGroupId = (int)$userGroupId; - } - public function getUserProfileId(): int { return (int)$this->userProfileId; } - public function setUserProfileId(int $userProfileId) - { - $this->userProfileId = (int)$userProfileId; - } - public function isAdminApp(): int { return (int)$this->isAdminApp; } - public function setIsAdminApp(bool $isAdminApp) - { - $this->isAdminApp = $isAdminApp; - } - public function isAdminAcc(): int { return (int)$this->isAdminAcc; } - public function setIsAdminAcc(bool $isAdminAcc) - { - $this->isAdminAcc = $isAdminAcc; - } - public function isDisabled(): int { return (int)$this->isDisabled; } - public function setIsDisabled(bool $isDisabled) - { - $this->isDisabled = $isDisabled; - } - public function isChangePass(): int { return (int)$this->isChangePass; } - public function setIsChangePass(bool $isChangePass) - { - $this->isChangePass = $isChangePass; - } - public function isLdap(): int { return (int)$this->isLdap; } - public function setIsLdap(bool $isLdap) - { - $this->isLdap = $isLdap; - } - public function getLogin(): ?string { return $this->login; } - public function setLogin(string $login) - { - $this->login = $login; - } - public function getName(): ?string { return $this->name; } - public function setName(string $name) - { - $this->name = $name; - } - public function getUserGroupName(): ?string { return $this->userGroupName; @@ -207,9 +149,4 @@ class UserData extends UserPassData implements DataModelInterface { return $this->ssoLogin; } - - public function setSsoLogin(string $ssoLogin) - { - $this->ssoLogin = $ssoLogin; - } } diff --git a/lib/SP/DataModel/UserPreferencesData.php b/lib/SP/DataModel/UserPreferencesData.php index fc5ab5ca..d8df2855 100644 --- a/lib/SP/DataModel/UserPreferencesData.php +++ b/lib/SP/DataModel/UserPreferencesData.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2021, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,190 +24,67 @@ namespace SP\DataModel; +use SP\Domain\Common\Models\Model; + /** * Class UserPreferencesData * * @package SP\DataModel */ -class UserPreferencesData +class UserPreferencesData extends Model { - /** - * @var int - */ - public $user_id = 0; - /** - * Lenguaje del usuario - * - * @var string - */ - public $lang = ''; - /** - * Tema del usuario - * - * @var string - */ - public $theme = ''; - /** - * @var int - */ - public $resultsPerPage = 0; - /** - * @var bool - */ - public $accountLink = false; - /** - * @var bool - */ - public $sortViews = false; - /** - * @var bool - */ - public $topNavbar = false; - /** - * @var bool - */ - public $optionalActions = false; - /** - * @var bool - */ - public $resultsAsCards = false; - /** - * @var bool - */ - public $checkNotifications = true; - /** - * @var bool - */ - public $showAccountSearchFilters = false; + protected ?string $lang = null; + protected ?string $theme = null; + protected int $resultsPerPage = 0; + protected bool $accountLink = false; + protected bool $sortViews = false; + protected bool $topNavbar = false; + protected bool $optionalActions = false; + protected bool $resultsAsCards = false; + protected bool $checkNotifications = true; + protected bool $showAccountSearchFilters = false; + protected ?int $user_id = null; - /** - * @return string - */ - public function getLang() + public function getLang(): ?string { return $this->lang; } - /** - * @param string $lang - */ - public function setLang($lang) - { - $this->lang = $lang; - } - - /** - * @return string - */ - public function getTheme() + public function getTheme(): ?string { return $this->theme; } - /** - * @param string $theme - */ - public function setTheme($theme) - { - $this->theme = $theme; - } - - /** - * @return int - */ - public function getResultsPerPage() + public function getResultsPerPage(): int { return $this->resultsPerPage; } - /** - * @param int $resultsPerPage - */ - public function setResultsPerPage($resultsPerPage) - { - $this->resultsPerPage = $resultsPerPage; - } - - /** - * @return boolean - */ - public function isAccountLink() + public function isAccountLink(): bool { return $this->accountLink; } - /** - * @param boolean $accountLink - */ - public function setAccountLink($accountLink) - { - $this->accountLink = $accountLink; - } - - /** - * @return boolean - */ - public function isSortViews() + public function isSortViews(): bool { return $this->sortViews; } - /** - * @param boolean $sortViews - */ - public function setSortViews($sortViews) - { - $this->sortViews = $sortViews; - } - - /** - * @return boolean - */ - public function isTopNavbar() + public function isTopNavbar(): bool { return $this->topNavbar; } - /** - * @param boolean $topNavbar - */ - public function setTopNavbar($topNavbar) - { - $this->topNavbar = $topNavbar; - } - - /** - * @return boolean - */ - public function isOptionalActions() + public function isOptionalActions(): bool { return $this->optionalActions; } - /** - * @param boolean $optionalActions - */ - public function setOptionalActions($optionalActions) - { - $this->optionalActions = $optionalActions; - } - - /** - * @return int - */ - public function getUserId() + public function getUserId(): ?int { return $this->user_id; } - /** - * @param int $user_id - */ - public function setUserId($user_id) - { - $this->user_id = $user_id; - } - /** * unserialize() checks for the presence of a function with the magic name __wakeup. * If present, this function can reconstruct any resources that the object may have. @@ -221,7 +98,7 @@ class UserPreferencesData { // Para realizar la conversión de nombre de propiedades que empiezan por _ foreach (get_object_vars($this) as $name => $value) { - if (strpos($name, '_') === 0) { + if (str_starts_with($name, '_')) { $newName = substr($name, 1); $this->$newName = $value; @@ -231,51 +108,18 @@ class UserPreferencesData } } - /** - * @return bool - */ - public function isResultsAsCards() + public function isResultsAsCards(): bool { return $this->resultsAsCards; } - /** - * @param bool $resultsAsCards - */ - public function setResultsAsCards($resultsAsCards) - { - $this->resultsAsCards = $resultsAsCards; - } - - /** - * @return bool - */ public function isCheckNotifications(): bool { return $this->checkNotifications; } - /** - * @param bool $checkNotifications - */ - public function setCheckNotifications(bool $checkNotifications) - { - $this->checkNotifications = $checkNotifications; - } - - /** - * @return bool - */ public function isShowAccountSearchFilters(): bool { return $this->showAccountSearchFilters; } - - /** - * @param bool $showAccountSearchFilters - */ - public function setShowAccountSearchFilters(bool $showAccountSearchFilters) - { - $this->showAccountSearchFilters = $showAccountSearchFilters; - } -} \ No newline at end of file +} diff --git a/lib/SP/Domain/Api/Ports/ApiRequestInterface.php b/lib/SP/Domain/Api/Ports/ApiRequestInterface.php new file mode 100644 index 00000000..6d200abe --- /dev/null +++ b/lib/SP/Domain/Api/Ports/ApiRequestInterface.php @@ -0,0 +1,72 @@ +. + */ + +namespace SP\Domain\Api\Ports; + +/** + * Class ApiRequest + * + * @package SP\Domain\Api\Services + */ +interface ApiRequestInterface +{ + public const PHP_REQUEST_STREAM = 'php://input'; + + /** + * Build the ApiRequest from the request itself. + * + * It will read the 'php://input' strean and get the contents into a JSON format + * + * @param string $stream + * + * @return ApiRequestInterface + * @throws \SP\Domain\Api\Services\ApiRequestException + */ + public static function buildFromRequest(string $stream = self::PHP_REQUEST_STREAM): ApiRequestInterface; + + /** + * @param string $key + * @param mixed|null $default + * + * @return mixed + */ + public function get(string $key, mixed $default = null): mixed; + + /** + * @param string $key + * + * @return bool + */ + public function exists(string $key): bool; + + /** + * @return string + */ + public function getMethod(): string; + + /** + * @return int + */ + public function getId(): int; +} diff --git a/lib/SP/Domain/Api/Ports/ApiServiceInterface.php b/lib/SP/Domain/Api/Ports/ApiServiceInterface.php index db0256c8..7ee39768 100644 --- a/lib/SP/Domain/Api/Ports/ApiServiceInterface.php +++ b/lib/SP/Domain/Api/Ports/ApiServiceInterface.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,12 +24,10 @@ namespace SP\Domain\Api\Ports; - use Exception; use SP\Core\Context\ContextException; use SP\Core\Exceptions\InvalidClassException; use SP\Core\Exceptions\SPException; -use SP\Domain\Api\Services\ApiRequest; use SP\Domain\Common\Services\ServiceException; /** @@ -53,21 +51,12 @@ interface ApiServiceInterface * * @param string $param * @param bool $required Si es requerido - * @param mixed $default Valor por defecto + * @param mixed|null $default Valor por defecto * * @return mixed * @throws \SP\Domain\Common\Services\ServiceException */ - public function getParam(string $param, bool $required = false, $default = null); - - /** - * Devuelve la ayuda para una acción - * - * @param string $action - * - * @return array - */ - public function getHelp(string $action): array; + public function getParam(string $param, bool $required = false, mixed $default = null): mixed; /** * @throws ServiceException @@ -100,12 +89,8 @@ interface ApiServiceInterface */ public function getMasterPass(): string; - public function setApiRequest(ApiRequest $apiRequest): ApiServiceInterface; - public function getRequestId(): int; - public function isInitialized(): bool; - /** * @throws InvalidClassException */ diff --git a/lib/SP/Domain/Api/Services/ApiRequest.php b/lib/SP/Domain/Api/Services/ApiRequest.php index d697f2d9..50021551 100644 --- a/lib/SP/Domain/Api/Services/ApiRequest.php +++ b/lib/SP/Domain/Api/Services/ApiRequest.php @@ -26,6 +26,7 @@ namespace SP\Domain\Api\Services; use JsonException; use SP\Core\Exceptions\SPException; +use SP\Domain\Api\Ports\ApiRequestInterface; use function SP\__u; /** @@ -33,10 +34,8 @@ use function SP\__u; * * @package SP\Domain\Api\Services */ -final class ApiRequest +final class ApiRequest implements ApiRequestInterface { - private const PHP_REQUEST_STREAM = 'php://input'; - protected ?string $method = null; protected ?int $id = null; protected ?ApiRequestData $data = null; @@ -50,10 +49,10 @@ final class ApiRequest * * @param string $stream * - * @return \SP\Domain\Api\Services\ApiRequest + * @return ApiRequestInterface * @throws \SP\Domain\Api\Services\ApiRequestException */ - public static function buildFromRequest(string $stream = self::PHP_REQUEST_STREAM): ApiRequest + public static function buildFromRequest(string $stream = self::PHP_REQUEST_STREAM): ApiRequestInterface { $content = file_get_contents($stream); @@ -74,10 +73,10 @@ final class ApiRequest * * @param string $json * - * @return ApiRequest + * @return ApiRequestInterface * @throws \SP\Domain\Api\Services\ApiRequestException */ - private static function buildFromJson(string $json): ApiRequest + private static function buildFromJson(string $json): ApiRequestInterface { try { $data = json_decode( diff --git a/lib/SP/Domain/Api/Services/ApiService.php b/lib/SP/Domain/Api/Services/ApiService.php index 9f656a44..1a9202ae 100644 --- a/lib/SP/Domain/Api/Services/ApiService.php +++ b/lib/SP/Domain/Api/Services/ApiService.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -24,28 +24,33 @@ namespace SP\Domain\Api\Services; -use Defuse\Crypto\Exception\CryptoException; use Exception; use SP\Core\Application; -use SP\Core\Context\ContextException; +use SP\Core\Context\ContextInterface; +use SP\Core\Crypt\Crypt; use SP\Core\Crypt\Hash; use SP\Core\Crypt\Vault; +use SP\Core\Exceptions\CryptException; use SP\Core\Exceptions\InvalidClassException; use SP\Core\Exceptions\SPException; use SP\DataModel\AuthTokenData; +use SP\Domain\Api\Ports\ApiRequestInterface; use SP\Domain\Api\Ports\ApiServiceInterface; use SP\Domain\Auth\Ports\AuthTokenServiceInterface; use SP\Domain\Auth\Services\AuthTokenService; use SP\Domain\Common\Services\Service; use SP\Domain\Common\Services\ServiceException; use SP\Domain\Security\Ports\TrackServiceInterface; -use SP\Domain\Security\Services\TrackService; use SP\Domain\User\Ports\UserProfileServiceInterface; use SP\Domain\User\Ports\UserServiceInterface; use SP\Domain\User\Services\UserService; use SP\Infrastructure\Common\Repositories\NoSuchItemException; use SP\Infrastructure\Security\Repositories\TrackRequest; +use SP\Modules\Api\Controllers\Help\HelpInterface; use SP\Util\Filter; +use function SP\__u; +use function SP\logger; +use function SP\processException; /** * Class ApiService @@ -54,33 +59,29 @@ use SP\Util\Filter; */ final class ApiService extends Service implements ApiServiceInterface { - private AuthTokenService $authTokenService; - private TrackService $trackService; - private UserServiceInterface $userService; - private UserProfileServiceInterface $userProfileService; - private ApiRequest $apiRequest; - private TrackRequest $trackRequest; - private ?AuthTokenData $authTokenData = null; - private ?string $helpClass = null; - private $initialized = false; + private const STATUS_INITIALIZED = 0; + private const STATUS_INITIALIZING = 1; + private TrackServiceInterface $trackService; + private TrackRequest $trackRequest; + private ?AuthTokenData $authTokenData = null; + private ?string $helpClass = null; + private ?int $status = null; + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + */ public function __construct( Application $application, - ApiRequest $apiRequest, TrackServiceInterface $trackService, - AuthTokenServiceInterface $authTokenService, - UserServiceInterface $userService, - UserProfileServiceInterface $userProfileService + private ApiRequestInterface $apiRequest, + private AuthTokenServiceInterface $authTokenService, + private UserServiceInterface $userService, + private UserProfileServiceInterface $userProfileService ) { parent::__construct($application); - $this->apiRequest = $apiRequest; $this->trackService = $trackService; - $this->authTokenService = $authTokenService; - $this->userService = $userService; - $this->userProfileService = $userProfileService; - - $this->trackRequest = $this->trackService->getTrackRequest(__CLASS__); + $this->trackRequest = $trackService->getTrackRequest(__CLASS__); } /** @@ -92,7 +93,7 @@ final class ApiService extends Service implements ApiServiceInterface */ public function setup(int $actionId): void { - $this->initialized = false; + $this->status = self::STATUS_INITIALIZING; if ($this->trackService->checkTracking($this->trackRequest)) { $this->addTracking(); @@ -130,7 +131,7 @@ final class ApiService extends Service implements ApiServiceInterface $this->requireMasterPass(); } - $this->initialized = true; + $this->status = self::STATUS_INITIALIZED; } /** @@ -143,6 +144,8 @@ final class ApiService extends Service implements ApiServiceInterface try { $this->trackService->add($this->trackRequest); } catch (Exception $e) { + processException($e); + throw new ServiceException( __u('Internal error'), SPException::ERROR, @@ -157,18 +160,18 @@ final class ApiService extends Service implements ApiServiceInterface * * @param string $param * @param bool $required Si es requerido - * @param mixed $default Valor por defecto + * @param mixed|null $default Valor por defecto * * @return mixed * @throws ServiceException */ - public function getParam(string $param, bool $required = false, $default = null) + public function getParam(string $param, bool $required = false, mixed $default = null): mixed { if ($required && !$this->apiRequest->exists($param)) { throw new ServiceException( __u('Wrong parameters'), SPException::ERROR, - $this->getHelp($this->apiRequest->getMethod()), + join(PHP_EOL, $this->getHelp($this->apiRequest->getMethod())), JsonRpcResponse::INVALID_PARAMS ); } @@ -183,7 +186,7 @@ final class ApiService extends Service implements ApiServiceInterface * * @return array */ - public function getHelp(string $action): array + private function getHelp(string $action): array { if ($this->helpClass !== null) { return call_user_func([$this->helpClass, 'getHelpFor'], $action); @@ -226,12 +229,12 @@ final class ApiService extends Service implements ApiServiceInterface } /** + * @throws \SP\Core\Context\ContextException * @throws \SP\Domain\Common\Services\ServiceException - * @throws ContextException */ public function requireMasterPass(): void { - $this->context->setTrasientKey('_masterpass', $this->getMasterPassFromVault()); + $this->context->setTrasientKey(ContextInterface::MASTER_PASSWORD_KEY, $this->getMasterPassFromVault()); } /** @@ -241,15 +244,19 @@ final class ApiService extends Service implements ApiServiceInterface */ private function getMasterPassFromVault(): string { + $this->requireInitialized(); + try { $tokenPass = $this->getParam('tokenPass', true); Hash::checkHashKey($tokenPass, $this->authTokenData->getHash()) || $this->accessDenied(); /** @var Vault $vault */ - $vault = unserialize($this->authTokenData->getVault()); + $vault = unserialize($this->authTokenData->getVault(), ['allowed_classes' => [Vault::class, Crypt::class]]); - if ($vault && ($pass = $vault->getData($tokenPass.$this->getParam('authToken')))) { + $key = sha1($tokenPass.$this->getParam('authToken')); + + if ($vault && ($pass = $vault->getData($key))) { return $pass; } @@ -259,7 +266,7 @@ final class ApiService extends Service implements ApiServiceInterface __u('Invalid data'), JsonRpcResponse::INTERNAL_ERROR ); - } catch (CryptoException $e) { + } catch (CryptException $e) { throw new ServiceException( __u('Internal error'), SPException::ERROR, @@ -269,6 +276,21 @@ final class ApiService extends Service implements ApiServiceInterface } } + /** + * @throws \SP\Domain\Common\Services\ServiceException + */ + private function requireInitialized(): void + { + if ($this->status === null) { + throw new ServiceException( + __u('API not initialized'), + SPException::ERROR, + __u('Please run setup method before'), + JsonRpcResponse::INTERNAL_ERROR + ); + } + } + /** * @throws \SP\Domain\Common\Services\ServiceException */ @@ -319,7 +341,7 @@ final class ApiService extends Service implements ApiServiceInterface $value = $this->getParam($param, $required, $default); if (null !== $value) { - return Filter::getRaw($value); + return $value; } return $default; @@ -330,32 +352,22 @@ final class ApiService extends Service implements ApiServiceInterface */ public function getMasterPass(): string { + $this->requireInitialized(); + return $this->getMasterKeyFromContext(); } - public function setApiRequest(ApiRequest $apiRequest): ApiServiceInterface - { - $this->apiRequest = $apiRequest; - - return $this; - } - public function getRequestId(): int { return $this->apiRequest->getId(); } - public function isInitialized(): bool - { - return $this->initialized; - } - /** * @throws InvalidClassException */ public function setHelpClass(string $helpClass): void { - if (class_exists($helpClass)) { + if (class_exists($helpClass) && is_subclass_of($helpClass, HelpInterface::class)) { $this->helpClass = $helpClass; return; diff --git a/lib/SP/Domain/Install/Services/InstallerService.php b/lib/SP/Domain/Install/Services/InstallerService.php index 6de35f44..1e3839d0 100644 --- a/lib/SP/Domain/Install/Services/InstallerService.php +++ b/lib/SP/Domain/Install/Services/InstallerService.php @@ -5,7 +5,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -264,15 +264,15 @@ final class InstallerService implements InstallerServiceInterface private function setupConfig(): ConfigDataInterface { $configData = $this->config->getConfigData() - ->setConfigVersion(VersionUtil::getVersionStringNormalized()) - ->setDatabaseVersion(VersionUtil::getVersionStringNormalized()) - ->setAppVersion(VersionUtil::getVersionStringNormalized()) - ->setUpgradeKey(null) - ->setDbHost($this->installData->getDbHost()) - ->setDbSocket($this->installData->getDbSocket()) - ->setDbPort($this->installData->getDbPort()) - ->setDbName($this->installData->getDbName()) - ->setSiteLang($this->installData->getSiteLang()); + ->setConfigVersion(VersionUtil::getVersionStringNormalized()) + ->setDatabaseVersion(VersionUtil::getVersionStringNormalized()) + ->setAppVersion(VersionUtil::getVersionStringNormalized()) + ->setUpgradeKey(null) + ->setDbHost($this->installData->getDbHost()) + ->setDbSocket($this->installData->getDbSocket()) + ->setDbPort($this->installData->getDbPort()) + ->setDbName($this->installData->getDbName()) + ->setSiteLang($this->installData->getSiteLang()); $this->config->updateConfig($configData); @@ -343,12 +343,13 @@ final class InstallerService implements InstallerServiceInterface $userProfileData = new UserProfileData(['name' => 'Admin', 'profile' => new ProfileData()]); - $userData = new UserData(); - $userData->setUserGroupId($this->userGroupService->create($userGroupData)); - $userData->setUserProfileId($this->userProfileService->create($userProfileData)); - $userData->setLogin($this->installData->getAdminLogin()); - $userData->setName('sysPass Admin'); - $userData->setIsAdminApp(1); + $userData = new UserData([ + 'userGroupId' => $this->userGroupService->create($userGroupData), + 'userProfileId' => $this->userProfileService->create($userProfileData), + 'login' => $this->installData->getAdminLogin(), + 'name' => 'sysPass Admin', + 'isAdminApp' => 1, + ]); $id = $this->userService->createWithMasterPass( $userData, diff --git a/lib/SP/Util/Filter.php b/lib/SP/Util/Filter.php index 67908b26..271b9786 100644 --- a/lib/SP/Util/Filter.php +++ b/lib/SP/Util/Filter.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -76,7 +76,7 @@ final class Filter public static function getString(?string $value): string { - return filter_var(trim($value), FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES); + return htmlspecialchars(trim($value), ENT_NOQUOTES | ENT_SUBSTITUTE | ENT_HTML401); } public static function getRaw(string $value): string diff --git a/tests/SP/Domain/Account/Search/AccountSearchDataBuilderTest.php b/tests/SP/Domain/Account/Search/AccountSearchDataBuilderTest.php index 322df4b7..06088f5d 100644 --- a/tests/SP/Domain/Account/Search/AccountSearchDataBuilderTest.php +++ b/tests/SP/Domain/Account/Search/AccountSearchDataBuilderTest.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -62,7 +62,7 @@ class AccountSearchDataBuilderTest extends UnitaryTestCase public function testBuildFrom(): void { $accountSearchVData = - array_map(static fn() => AccountDataGenerator::factory()->builAccountSearchView(), range(0, 4)); + array_map(static fn() => AccountDataGenerator::factory()->buildAccountSearchView(), range(0, 4)); $numResults = count($accountSearchVData); $queryResult = new QueryResult($accountSearchVData); @@ -100,7 +100,7 @@ class AccountSearchDataBuilderTest extends UnitaryTestCase public function testBuildFromWithColorCacheException(): void { $accountSearchVData = - array_map(static fn() => AccountDataGenerator::factory()->builAccountSearchView(), range(0, 4)); + array_map(static fn() => AccountDataGenerator::factory()->buildAccountSearchView(), range(0, 4)); $numResults = count($accountSearchVData); $queryResult = new QueryResult($accountSearchVData); diff --git a/tests/SP/Domain/Account/Services/AccountServiceTest.php b/tests/SP/Domain/Account/Services/AccountServiceTest.php index 6cf0be4d..98c8dae4 100644 --- a/tests/SP/Domain/Account/Services/AccountServiceTest.php +++ b/tests/SP/Domain/Account/Services/AccountServiceTest.php @@ -476,7 +476,7 @@ class AccountServiceTest extends UnitaryTestCase public function testGetByIdEnriched() { $id = self::$faker->randomNumber(); - $accountDataView = AccountDataGenerator::factory()->builAccountDataView(); + $accountDataView = AccountDataGenerator::factory()->buildAccountDataView(); $result = new QueryResult([$accountDataView]); $this->accountRepository->expects(self::once())->method('getByIdEnriched') diff --git a/tests/SP/Domain/Api/Services/ApiServiceTest.php b/tests/SP/Domain/Api/Services/ApiServiceTest.php new file mode 100644 index 00000000..d6da7982 --- /dev/null +++ b/tests/SP/Domain/Api/Services/ApiServiceTest.php @@ -0,0 +1,674 @@ +. + */ + +namespace SP\Tests\Domain\Api\Services; + +use Exception; +use Faker\Factory; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionClass; +use SP\Core\Acl\ActionsInterface; +use SP\Core\Context\ContextInterface; +use SP\Core\Crypt\Crypt; +use SP\Core\Crypt\Vault; +use SP\Core\Exceptions\InvalidClassException; +use SP\DataModel\AuthTokenData; +use SP\Domain\Api\Ports\ApiRequestInterface; +use SP\Domain\Api\Services\ApiService; +use SP\Domain\Auth\Ports\AuthTokenServiceInterface; +use SP\Domain\Common\Services\ServiceException; +use SP\Domain\Security\Ports\TrackServiceInterface; +use SP\Domain\User\Ports\UserProfileServiceInterface; +use SP\Domain\User\Ports\UserServiceInterface; +use SP\Infrastructure\Common\Repositories\NoSuchItemException; +use SP\Infrastructure\Security\Repositories\TrackRequest; +use SP\Modules\Api\Controllers\Help\AccountHelp; +use SP\Tests\Generators\UserDataGenerator; +use SP\Tests\Generators\UserProfileDataGenerator; +use SP\Tests\UnitaryTestCase; +use stdClass; +use function PHPUnit\Framework\onConsecutiveCalls; + +/** + * Class ApiServiceTest + * + * @group unitary + */ +class ApiServiceTest extends UnitaryTestCase +{ + + private TrackServiceInterface|MockObject $trackService; + private ApiRequestInterface|MockObject $apiRequest; + private AuthTokenServiceInterface|MockObject $authTokenService; + private UserServiceInterface|MockObject $userService; + private MockObject|UserProfileServiceInterface $userProfileService; + private ApiService $apiService; + private TrackRequest $trackRequest; + + /** + * @dataProvider getParamDataProvider + * + * @param mixed $value + * @param mixed $expected + * @param bool $required + * @param bool $present + */ + public function testGetParam(mixed $value, mixed $expected, bool $required, bool $present) + { + $this->checkParam([$this->apiService, 'getParam'], ...func_get_args()); + } + + private function checkParam( + callable $callable, + mixed $value, + mixed $expected, + bool $required, + bool $present + ): void { + $param = self::$faker->colorName; + + if ($required) { + $this->apiRequest->expects(self::once())->method('exists')->with($param)->willReturn($present); + } + + if (!$present) { + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Wrong parameters'); + + $callable($param, true); + } else { + $this->apiRequest->expects(self::once())->method('get')->with($param)->willReturn($value); + + $out = $callable($param, $required, $expected); + + $this->assertEquals($expected, $out); + } + } + + /** + * @throws \SP\Core\Exceptions\InvalidClassException + * @throws \SP\Core\Exceptions\InvalidArgumentException + */ + public function testGetParamWithHelp() + { + $apiRequest = $this->createMock(ApiRequestInterface::class); + $apiRequest->method('exists')->willReturn(false); + $apiRequest->method('getMethod')->willReturn('account/view'); + + $apiService = new ApiService( + $this->application, + $this->trackService, + $apiRequest, + $this->authTokenService, + $this->userService, + $this->userProfileService + ); + + $apiService->setHelpClass(AccountHelp::class); + + try { + $apiService->getParam(self::$faker->colorName, true); + } catch (ServiceException $e) { + $this->assertNotEmpty($e->getHint()); + } + } + + /** + * @throws \SP\Core\Exceptions\InvalidClassException + */ + public function testSetHelpClass() + { + $this->apiService->setHelpClass(AccountHelp::class); + + $reflection = new ReflectionClass($this->apiService); + $property = $reflection->getProperty('helpClass'); + + $this->assertEquals(AccountHelp::class, $property->getValue($this->apiService)); + } + + /** + * @throws \SP\Core\Exceptions\InvalidClassException + */ + public function testSetHelpClassError() + { + $this->expectException(InvalidClassException::class); + $this->expectExceptionMessage('Invalid class for helper'); + + $this->apiService->setHelpClass(stdClass::class); + } + + /** + * @dataProvider getParamIntDataProvider + */ + public function testGetParamInt(mixed $value, mixed $expected, bool $required, bool $present) + { + $this->checkParam([$this->apiService, 'getParamInt'], ...func_get_args()); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetup() + { + $actionId = self::$faker->randomNumber(5); + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(false); + + $authToken = self::$faker->password; + + $this->apiRequest->expects(self::once())->method('get')->with('authToken')->willReturn($authToken); + + $userId = self::$faker->randomNumber(); + + $authTokenData = new AuthTokenData(['actionId' => $actionId, 'userId' => $userId]); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $userData = UserDataGenerator::factory()->buildUserData()->mutate(['isDisabled' => false]); + + $this->userService->expects(self::once())->method('getById')->with($userId)->willReturn($userData); + $this->userProfileService->expects(self::once()) + ->method('getById') + ->with($userData->getUserProfileId()) + ->willReturn(UserProfileDataGenerator::factory()->buildUserProfileData()); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Core\Exceptions\InvalidArgumentException + * @throws \SP\Core\Context\ContextException + */ + protected function setUp(): void + { + parent::setUp(); + + $this->trackService = $this->createMock(TrackServiceInterface::class); + $this->apiRequest = $this->createMock(ApiRequestInterface::class); + $this->authTokenService = $this->createMock(AuthTokenServiceInterface::class); + $this->userService = $this->createMock(UserServiceInterface::class); + $this->userProfileService = $this->createMock(UserProfileServiceInterface::class); + + $this->trackRequest = new TrackRequest(time(), __CLASS__); + $this->trackService->method('getTrackRequest')->willReturn($this->trackRequest); + $this->apiRequest->method('getMethod')->willReturn(self::$faker->colorName); + + $this->apiService = new ApiService( + $this->application, + $this->trackService, + $this->apiRequest, + $this->authTokenService, + $this->userService, + $this->userProfileService + ); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupAttemptsExceeded() + { + $actionId = self::$faker->randomNumber(); + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(true); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Attempts exceeded'); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupTrackingError() + { + $actionId = self::$faker->randomNumber(); + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(true); + + $this->trackService + ->expects(self::once()) + ->method('add') + ->willThrowException(new Exception()); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Internal error'); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupInvalidToken() + { + $actionId = self::$faker->randomNumber(); + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(false); + + $this->apiRequest + ->expects(self::once()) + ->method('get') + ->with('authToken') + ->willThrowException(new NoSuchItemException('test')); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Internal error'); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupAccessDenied() + { + $actionId = self::$faker->randomNumber(); + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(false); + + $authToken = self::$faker->password; + + $this->apiRequest->expects(self::once())->method('get')->with('authToken')->willReturn($authToken); + + $userId = self::$faker->randomNumber(); + + $authTokenData = new AuthTokenData(['actionId' => self::$faker->randomNumber(), 'userId' => $userId]); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $this->trackService->expects(self::once())->method('add')->with($this->trackRequest); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Unauthorized access'); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupWithMasterPass() + { + $actionId = ActionsInterface::ACCOUNT_VIEW_PASS; + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(false); + + $authToken = self::$faker->password; + $authTokenHash = password_hash($authToken, PASSWORD_BCRYPT); + + $this->apiRequest->expects(self::exactly(3)) + ->method('get') + ->will(onConsecutiveCalls($authToken, $authToken, $authToken)); + + $vaultKey = sha1($authToken.$authToken); + + $vault = Vault::factory(new Crypt())->saveData(self::$faker->password, $vaultKey); + + $userId = self::$faker->randomNumber(); + + $authTokenData = + new AuthTokenData( + ['actionId' => $actionId, 'userId' => $userId, 'hash' => $authTokenHash, 'vault' => serialize($vault)] + ); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $userData = UserDataGenerator::factory()->buildUserData()->mutate(['isDisabled' => false]); + + $this->userService->expects(self::once())->method('getById')->with($userId)->willReturn($userData); + $this->userProfileService->expects(self::once()) + ->method('getById') + ->with($userData->getUserProfileId()) + ->willReturn(UserProfileDataGenerator::factory()->buildUserProfileData()); + + $this->apiRequest->expects(self::once())->method('exists')->with('tokenPass')->willReturn(true); + + $this->apiService->setup($actionId); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testSetupWithMasterPassWrongTokenPass() + { + $actionId = ActionsInterface::ACCOUNT_VIEW_PASS; + + $this->trackService + ->expects(self::once()) + ->method('checkTracking') + ->with($this->trackRequest) + ->willReturn(false); + + $authToken = self::$faker->password; + $authTokenHash = password_hash($authToken, PASSWORD_BCRYPT); + + $this->apiRequest->expects(self::exactly(3)) + ->method('get') + ->will(onConsecutiveCalls($authToken, $authToken, $authToken)); + + $vault = Vault::factory(new Crypt())->saveData(self::$faker->password, sha1(self::$faker->password)); + + $userId = self::$faker->randomNumber(); + + $authTokenData = + new AuthTokenData( + ['actionId' => $actionId, 'userId' => $userId, 'hash' => $authTokenHash, 'vault' => serialize($vault)] + ); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $userData = UserDataGenerator::factory()->buildUserData()->mutate(['isDisabled' => false]); + + $this->userService->expects(self::once())->method('getById')->with($userId)->willReturn($userData); + $this->userProfileService->expects(self::once()) + ->method('getById') + ->with($userData->getUserProfileId()) + ->willReturn(UserProfileDataGenerator::factory()->buildUserProfileData()); + + $this->apiRequest->expects(self::once())->method('exists')->with('tokenPass')->willReturn(true); + + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('Internal error'); + + $this->apiService->setup($actionId); + } + + /** + * @dataProvider getParamStringDataProvider + * + * @return void + */ + public function testGetParamString(mixed $value, mixed $expected, bool $required, bool $present) + { + $this->checkParam([$this->apiService, 'getParamString'], ...func_get_args()); + } + + /** + * @dataProvider getParamArrayDataProvider + * + * @return void + */ + public function testGetParamArray(mixed $value, mixed $expected, bool $required, bool $present) + { + $this->checkParam([$this->apiService, 'getParamArray'], ...func_get_args()); + } + + /** + * @dataProvider getParamRawDataProvider + * + * @return void + */ + public function testGetParamRaw(mixed $value, mixed $expected, bool $required, bool $present) + { + $this->checkParam([$this->apiService, 'getParamRaw'], ...func_get_args()); + } + + public function testGetRequestId() + { + $this->assertEquals($this->apiRequest->getId(), $this->apiService->getRequestId()); + } + + /** + * @throws \SP\Core\Context\ContextException + * @throws \SP\Core\Exceptions\CryptException + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testRequireMasterPass() + { + $actionId = self::$faker->randomNumber(); + $authToken = self::$faker->password; + $authTokenHash = password_hash($authToken, PASSWORD_BCRYPT); + + $this->apiRequest->expects(self::exactly(3)) + ->method('get') + ->willReturn($authToken); + + $vaultKey = sha1($authToken.$authToken); + + $masterPass = self::$faker->password; + + $vault = Vault::factory(new Crypt())->saveData($masterPass, $vaultKey); + + $userId = self::$faker->randomNumber(); + + $authTokenData = + new AuthTokenData( + ['actionId' => $actionId, 'userId' => $userId, 'hash' => $authTokenHash, 'vault' => serialize($vault)] + ); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $userData = UserDataGenerator::factory()->buildUserData()->mutate(['isDisabled' => false]); + + $this->userService->expects(self::once())->method('getById')->with($userId)->willReturn($userData); + $this->userProfileService->expects(self::once()) + ->method('getById') + ->with($userData->getUserProfileId()) + ->willReturn(UserProfileDataGenerator::factory()->buildUserProfileData()); + + $this->apiRequest->expects(self::once())->method('exists')->with('tokenPass')->willReturn(true); + + $this->apiService->setup($actionId); + $this->apiService->requireMasterPass(); + + $this->assertEquals($masterPass, $this->context->getTrasientKey(ContextInterface::MASTER_PASSWORD_KEY)); + } + + /** + * @throws \SP\Core\Context\ContextException + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\SPException + */ + public function testRequireMasterPassNotInitialized() + { + $this->expectException(ServiceException::class); + $this->expectExceptionMessage('API not initialized'); + + $this->apiService->requireMasterPass(); + } + + /** + * @throws \SP\Domain\Common\Services\ServiceException + * @throws \SP\Core\Exceptions\CryptException + * @throws \SP\Core\Exceptions\SPException + */ + public function testGetMasterPass() + { + $actionId = ActionsInterface::ACCOUNT_VIEW_PASS; + $authToken = self::$faker->password; + $authTokenHash = password_hash($authToken, PASSWORD_BCRYPT); + + $this->apiRequest->expects(self::exactly(3)) + ->method('get') + ->willReturn($authToken); + + $vaultKey = sha1($authToken.$authToken); + + $masterPass = self::$faker->password; + + $vault = Vault::factory(new Crypt())->saveData($masterPass, $vaultKey); + + $userId = self::$faker->randomNumber(); + + $authTokenData = + new AuthTokenData( + ['actionId' => $actionId, 'userId' => $userId, 'hash' => $authTokenHash, 'vault' => serialize($vault)] + ); + + $this->authTokenService + ->expects(self::once()) + ->method('getTokenByToken') + ->with($actionId, $authToken) + ->willReturn($authTokenData); + + $userData = UserDataGenerator::factory()->buildUserData()->mutate(['isDisabled' => false]); + + $this->userService->expects(self::once())->method('getById')->with($userId)->willReturn($userData); + $this->userProfileService->expects(self::once()) + ->method('getById') + ->with($userData->getUserProfileId()) + ->willReturn(UserProfileDataGenerator::factory()->buildUserProfileData()); + + $this->apiRequest->expects(self::once())->method('exists')->with('tokenPass')->willReturn(true); + + $this->apiService->setup($actionId); + + $this->assertEquals( + $this->apiService->getMasterPass(), + $this->context->getTrasientKey(ContextInterface::MASTER_PASSWORD_KEY) + ); + } + + private function getParamIntDataProvider(): array + { + $faker = Factory::create(); + $number = $faker->randomNumber(); + + return [ + [$number, $number, false, true], + [$number, $number, true, true], + [$number, $number, true, false], + [(string)$number, $number, false, true], + [$faker->colorName, null, false, true], + [null, $faker->randomNumber(), false, true], + ]; + } + + private function getParamStringDataProvider(): array + { + $faker = Factory::create(); + $string = $faker->colorName; + + // mixed $value, mixed $expected, bool $required, bool $present + return [ + [$string, $string, false, true], + [$string, $string, true, true], + [$string, $string, true, false], + [null, null, false, true], + [null, $faker->colorName, false, true], + ]; + } + + private function getParamDataProvider(): array + { + $faker = Factory::create(); + $string = $faker->colorName; + + // mixed $value, mixed $expected, bool $required, bool $present + return [ + [$string, $string, false, true], + [$string, $string, true, true], + [$string, $string, true, false], + [$string, $string, false, false], + ]; + } + + private function getParamArrayDataProvider(): array + { + $faker = Factory::create(); + $numbers = array_map(fn() => $faker->randomNumber(), range(0, 4)); + $strings = array_map(fn() => $faker->colorName, range(0, 4)); + + // mixed $value, mixed $expected, bool $required, bool $present + return [ + [$numbers, $numbers, false, true], + [$strings, $strings, false, true], + [$numbers, $numbers, true, true], + [$strings, $strings, true, true], + [$numbers, $numbers, true, false], + [$strings, $strings, true, false], + [$numbers, $numbers, false, false], + [$strings, $strings, false, false], + [null, null, false, false], + ]; + } + + private function getParamRawDataProvider(): array + { + $faker = Factory::create(); + $password = $faker->password; + + // mixed $value, mixed $expected, bool $required, bool $present + return [ + [$password, $password, false, true], + [$password, $password, true, true], + [$password, $password, true, false], + [$password, $password, false, false], + [null, null, false, false], + ]; + } +} diff --git a/tests/SP/Generators/AccountDataGenerator.php b/tests/SP/Generators/AccountDataGenerator.php index 67e1416a..60083523 100644 --- a/tests/SP/Generators/AccountDataGenerator.php +++ b/tests/SP/Generators/AccountDataGenerator.php @@ -4,7 +4,7 @@ * * @author nuxsmin * @link https://syspass.org - * @copyright 2012-2022, Rubén Domínguez nuxsmin@$syspass.org + * @copyright 2012-2023, Rubén Domínguez nuxsmin@$syspass.org * * This file is part of sysPass. * @@ -41,14 +41,14 @@ final class AccountDataGenerator extends DataGenerator { public function buildAccountEnrichedDto(): AccountEnrichedDto { - $out = new AccountEnrichedDto($this->builAccountDataView()); + $out = new AccountEnrichedDto($this->buildAccountDataView()); $out = $out->withUsers($this->buildItemData()); $out = $out->withTags($this->buildItemData()); return $out->withUserGroups($this->buildItemData()); } - public function builAccountDataView(): AccountDataView + public function buildAccountDataView(): AccountDataView { return new AccountDataView($this->getAccountProperties()); } @@ -104,7 +104,7 @@ final class AccountDataGenerator extends DataGenerator ); } - public function builAccountSearchView(): AccountSearchView + public function buildAccountSearchView(): AccountSearchView { return new AccountSearchView( array_merge( diff --git a/tests/SP/Generators/UserDataGenerator.php b/tests/SP/Generators/UserDataGenerator.php new file mode 100644 index 00000000..9a6f681b --- /dev/null +++ b/tests/SP/Generators/UserDataGenerator.php @@ -0,0 +1,111 @@ +. + */ + +namespace SP\Tests\Generators; + +use SP\DataModel\UserData; +use SP\DataModel\UserPassData; +use SP\DataModel\UserPreferencesData; + +/** + * Class UserDataGenerator + */ +final class UserDataGenerator extends DataGenerator +{ + public function buildUserData(): UserData + { + return new UserData(array_merge($this->getUserProperties(), $this->getUserPassProperties())); + } + + /** + * @return array + */ + private function getUserProperties(): array + { + return [ + 'id' => $this->faker->randomNumber(), + 'name' => $this->faker->name, + 'email' => $this->faker->randomNumber(), + 'login' => $this->faker->name, + 'ssoLogin' => $this->faker->userName, + 'notes' => $this->faker->text, + 'userGroupId' => $this->faker->randomNumber(), + 'userGroupName' => $this->faker->name, + 'userProfileId' => $this->faker->randomNumber(), + 'isAdminApp' => $this->faker->boolean, + 'isAdminAcc' => $this->faker->boolean, + 'isDisabled' => $this->faker->boolean, + 'isChangePass' => $this->faker->boolean, + 'isChangedPass' => $this->faker->boolean, + 'isLdap' => $this->faker->boolean, + 'isMigrate' => $this->faker->boolean, + 'loginCount' => $this->faker->randomNumber(), + 'lastLogin' => $this->faker->unixTime, + 'lastUpdate' => $this->faker->unixTime, + 'preferences' => serialize($this->buildUserPreferencesData()), + ]; + } + + public function buildUserPreferencesData(): UserPreferencesData + { + return new UserPreferencesData($this->getUserPreferencesProperties()); + } + + private function getUserPreferencesProperties(): array + { + return [ + 'lang' => $this->faker->languageCode, + 'theme' => $this->faker->colorName, + 'resultsPerPage' => $this->faker->randomNumber(), + 'accountLink' => $this->faker->boolean, + 'sortViews' => $this->faker->boolean, + 'topNavbar' => $this->faker->boolean, + 'optionalActions' => $this->faker->boolean, + 'resultsAsCards' => $this->faker->boolean, + 'checkNotifications' => $this->faker->boolean, + 'showAccountSearchFilters' => $this->faker->boolean, + 'user_id' => $this->faker->randomNumber(), + ]; + } + + /** + * @return array + */ + private function getUserPassProperties(): array + { + return [ + 'id' => $this->faker->randomNumber(), + 'pass' => $this->faker->password, + 'hashSalt' => $this->faker->sha1, + 'mPass' => $this->faker->sha1, + 'mKey' => $this->faker->sha1, + 'lastUpdateMPass' => $this->faker->dateTime->getTimestamp(), + ]; + } + + public function buildUserPassData(): UserPassData + { + return new UserPassData($this->getUserPassProperties()); + } +} diff --git a/tests/SP/Generators/UserProfileDataGenerator.php b/tests/SP/Generators/UserProfileDataGenerator.php new file mode 100644 index 00000000..f96dc2d6 --- /dev/null +++ b/tests/SP/Generators/UserProfileDataGenerator.php @@ -0,0 +1,89 @@ +. + */ + +namespace SP\Tests\Generators; + +use SP\DataModel\ProfileData; +use SP\DataModel\UserProfileData; + +/** + * Class UserProfileDataGenerator + */ +final class UserProfileDataGenerator extends DataGenerator +{ + public function buildUserProfileData(): UserProfileData + { + return new UserProfileData($this->getUserProfileProperties()); + } + + private function getUserProfileProperties(): array + { + return [ + 'id' => $this->faker->randomNumber(), + 'name' => $this->faker->name, + 'profile' => $this->buildProfileData(), + ]; + } + + public function buildProfileData(): ProfileData + { + return new ProfileData($this->getProfileProperties()); + } + + private function getProfileProperties(): array + { + return [ + 'accView' => $this->faker->boolean, + 'accViewPass' => $this->faker->boolean, + 'accViewHistory' => $this->faker->boolean, + 'accEdit' => $this->faker->boolean, + 'accEditPass' => $this->faker->boolean, + 'accAdd' => $this->faker->boolean, + 'accDelete' => $this->faker->boolean, + 'accFiles' => $this->faker->boolean, + 'accPrivate' => $this->faker->boolean, + 'accPrivateGroup' => $this->faker->boolean, + 'accPermission' => $this->faker->boolean, + 'accPublicLinks' => $this->faker->boolean, + 'accGlobalSearch' => $this->faker->boolean, + 'configGeneral' => $this->faker->boolean, + 'configEncryption' => $this->faker->boolean, + 'configBackup' => $this->faker->boolean, + 'configImport' => $this->faker->boolean, + 'mgmUsers' => $this->faker->boolean, + 'mgmGroups' => $this->faker->boolean, + 'mgmProfiles' => $this->faker->boolean, + 'mgmCategories' => $this->faker->boolean, + 'mgmCustomers' => $this->faker->boolean, + 'mgmApiTokens' => $this->faker->boolean, + 'mgmPublicLinks' => $this->faker->boolean, + 'mgmAccounts' => $this->faker->boolean, + 'mgmTags' => $this->faker->boolean, + 'mgmFiles' => $this->faker->boolean, + 'mgmItemsPreset' => $this->faker->boolean, + 'evl' => $this->faker->boolean, + 'mgmCustomFields' => $this->faker->boolean, + ]; + } +}