diff --git a/CRM/Utils/System/Standalone.php b/CRM/Utils/System/Standalone.php index 8e348722673e..e7ffb56bbce4 100644 --- a/CRM/Utils/System/Standalone.php +++ b/CRM/Utils/System/Standalone.php @@ -15,7 +15,6 @@ * @copyright CiviCRM LLC https://civicrm.org/licensing */ -use Civi\Standalone\Security; use Civi\Standalone\SessionHandler; /** @@ -24,8 +23,52 @@ class CRM_Utils_System_Standalone extends CRM_Utils_System_Base { /** - * @internal + * Standalone uses a CiviCRM Extension, Standaloneusers, to provide user + * functionality + * + * This is great for modularity - but does mean that there are points in + * bootstrap / install / failure where the extension isn't available + * and we need to provide fallback/failsafe behaviours + * + * This function provides a general check for whether we are in such a + * scenario + * + * (In the future, alternative user-providing extensions may be available - in + * which case this check might need generalising. One possibility could be + * to use the Api4 User interface as a spec for what any extension must provide + * + * Then maybe the check could be if (class_exists(\Civi\Api4\User::class))? + * * @return bool + * Whether user extension is available + */ + protected function isUserExtensionAvailable(): bool { + if (!class_exists(\Civi\Api4\User::class)) { + return FALSE; + } + // TODO: the following would be be a better check, as sometimes during + // upgrade the User class can exist but the entity is not actually loaded + // + // HOWEVER: it currently causes a crash during the install phase. + // https://github.com/civicrm/civicrm-core/pull/31198 may help. + // + // if (!\Civi\Api4\Utils\CoreUtil::entityExists('User')) { + // return FALSE; + // } + + // authx function is required for standalone user system + if (!function_exists('_authx_uf')) { + return FALSE; + } + + return TRUE; + } + + /** + * @inheritdoc + * + * In Standalone the UF is CiviCRM, so we're never + * running without it */ public function isLoaded(): bool { return TRUE; @@ -43,29 +86,34 @@ public function getDefaultFileStorage() { /** * @inheritDoc - * - * Create a user in the CMS. - * - * @param array $params keys: - * - 'cms_name' - * - 'cms_pass' plaintext password - * - 'notify' boolean - * @param string $mailParam - * Name of the param which contains the email address. - * Because. Right. OK. That's what it is. - * - * @return int|bool - * uid if user was created, false otherwise */ - public function createUser(&$params, $mailParam) { - return Security::singleton()->createUser($params, $mailParam); + public function createUser(&$params, $emailParam) { + try { + $email = $params[$emailParam]; + $userID = \Civi\Api4\User::create(FALSE) + ->addValue('username', $params['cms_name']) + ->addValue('uf_name', $email) + ->addValue('password', $params['cms_pass']) + ->addValue('contact_id', $params['contact_id'] ?? NULL) + // ->addValue('uf_id', 0) // does not work without this. + ->execute()->single()['id']; + } + catch (\Exception $e) { + \Civi::log()->warning("Failed to create user '$email': " . $e->getMessage()); + return FALSE; + } + + return (int) $userID; } /** * @inheritDoc */ public function updateCMSName($ufID, $email) { - return Security::singleton()->updateCMSName($ufID, $email); + \Civi\Api4\User::update(FALSE) + ->addWhere('id', '=', $ufID) + ->addValue('uf_name', $email) + ->execute(); } /** @@ -205,14 +253,17 @@ public static function currentPath() { * @inheritDoc * Authenticate the user against the CMS db. * + * I think this is only used by CLI so setting the session + * doesn't make sense + * * @param string $name * The user name. * @param string $password * The password for the above user. * @param bool $loadCMSBootstrap - * Load cms bootstrap?. + * Not used in Standalone context * @param string $realPath - * Filename of script + * Not used in Standalone context * * @return array|bool * [contactID, ufID, unique string] else false if no auth @@ -221,13 +272,6 @@ public static function currentPath() { public function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realPath = NULL) { $authxLogin = authx_login(['flow' => 'login', 'cred' => 'Basic ' . base64_encode("{$name}:{$password}")]); - $user = \Civi\Api4\User::get(FALSE) - ->addWhere('id', '=', $authxLogin['userId']) - ->addWhere('is_active', '=', TRUE) - ->execute()->single(); - - Security::singleton()->applyLocaleFromUser($user); - // Note: random_int is more appropriate for cryptographical use than mt_rand // The long number is the max 32 bit value. return [$authxLogin['contactId'], $authxLogin['userId'], random_int(0, 2147483647)]; @@ -242,17 +286,23 @@ public function authenticate($name, $password, $loadCMSBootstrap = FALSE, $realP * @return int|null */ public function getUfId($username) { - return Security::singleton()->getUserIDFromUsername($username); + if (!$this->isUserExtensionAvailable()) { + return NULL; + } + return \Civi\Api4\User::get(FALSE) + ->addWhere('username', '=', $username) + ->execute() + ->first()['id'] ?? NULL; } /** - * Immediately stop script execution, log out the user and redirect to the home page. - * - * @deprecated - * This function should be removed in favor of linking to the CMS's logout page + * Immediately stop script execution and log out the user */ public function logout() { - return Security::singleton()->logoutUser(); + _authx_uf()->logoutSession(); + // redirect to the home page? + // breaks tests in standaloneusers-e2e + // \CRM_Utils_System::redirect('/civicrm/login'); } /** @@ -317,50 +367,74 @@ public function theme(&$content, $print = FALSE, $maintenance = FALSE) { public function loadBootStrap($params = [], $loadUser = TRUE, $throwError = TRUE, $realPath = NULL) { static $runOnce; - if (!isset($runOnce)) { - $runOnce = TRUE; - } - else { + if (isset($runOnce)) { + // we've already run return TRUE; } + // dont run again + $runOnce = TRUE; + if (!$loadUser) { return TRUE; } - $security = \Civi\Standalone\Security::singleton(); - if (!empty($params['uid'])) { - $user = $security->loadUserByID($params['uid']); + try { + if (!empty($params['uid'])) { + _authx_uf()->loginStateless($params['uid']); + return TRUE; + } + elseif (!empty($params['name'] && !empty($params['pass']))) { + // It seems from looking at the Drupal implementation, that + // if given username we expect a correct password. + + /** + * @throws CRM_Core_Exception if login unsuccessful + */ + $this->authenticate($params['name'], $params['pass']); + return TRUE; + } + else { + return FALSE; + } } - elseif (!empty($params['name'] && !empty($params['pass']))) { - // It seems from looking at the Drupal implementation, that - // if given username we expect a correct password. - $user = $security->loadUserByName($params['name']); - if ($user) { - if (!$security->checkPassword($params['pass'], $user['hashed_password'] ?? '')) { - return FALSE; - } + catch (\CRM_Core_Exception $e) { + // swallow any errors if $throwError is false + // (presume the expectation is these are login errors + // - though that isn't guaranteed?) + if (!$throwError) { + return FALSE; } + throw $e; } - if (!$user) { + } + + /** + * @inheritdoc + */ + public function loadUser($username) { + $userID = $this->getUfId($username) ?? NULL; + if (!$userID) { return FALSE; } - - $security->loginAuthenticatedUserRecord($user, FALSE); - + _authx_uf()->loginSession($userID); return TRUE; } - public function loadUser($username) { - $security = \Civi\Standalone\Security::singleton(); - $user = $security->loadUserByName($username); - if ($user) { - $security->loginAuthenticatedUserRecord($user, TRUE); - return TRUE; - } - else { + /** + * Load an active user by internal user ID. + * + * @return array|bool FALSE if not found. + */ + public function getUserById(int $userID) { + if (!$this->isUserExtensionAvailable()) { return FALSE; } + return \Civi\Api4\User::get(FALSE) + ->addWhere('id', '=', $userID) + ->addWhere('is_active', '=', TRUE) + ->execute() + ->first() ?: FALSE; } /** @@ -393,7 +467,7 @@ public function isFrontEndPage() { * @inheritDoc */ public function isUserLoggedIn() { - return Security::singleton()->isUserLoggedIn(); + return !empty($this->getLoggedInUfID()); } /** @@ -422,9 +496,15 @@ public function updateCategories() { /** * @inheritDoc + * + * If the User extension isn't available + * then no one is logged in */ public function getLoggedInUfID() { - return Security::singleton()->getLoggedInUfID(); + if (!$this->isUserExtensionAvailable()) { + return NULL; + } + return _authx_uf()->getCurrentUserId(); } /** @@ -454,52 +534,6 @@ public function setMessage($message) { CRM_Core_Session::setStatus('', $message, 'info'); } - /** - * I don't know why this needs to be here? Does it even? - * - * Helper function to extract path, query and route name from Civicrm URLs. - * - * For example, 'civicrm/contact/view?reset=1&cid=66' will be returned as: - * - * ``` - * array( - * 'path' => 'civicrm/contact/view', - * 'route' => 'civicrm.civicrm_contact_view', - * 'query' => array('reset' => '1', 'cid' => '66'), - * ); - * ``` - * - * @param string $url - * The url to parse. - * - * @return string[] - * The parsed url parts, containing 'path', 'route' and 'query'. - */ - public function parseUrl($url) { - $processed = ['path' => '', 'route_name' => '', 'query' => []]; - - // Remove leading '/' if it exists. - $url = ltrim($url, '/'); - - // Separate out the url into its path and query components. - $url = parse_url($url); - if (empty($url['path'])) { - return $processed; - } - $processed['path'] = $url['path']; - - // Create a route name by replacing the forward slashes in the path with - // underscores, civicrm/contact/search => civicrm.civicrm_contact_search. - $processed['route_name'] = 'civicrm.' . implode('_', explode('/', $url['path'])); - - // Turn the query string (if it exists) into an associative array. - if (!empty($url['query'])) { - parse_str($url['query'], $processed['query']); - } - - return $processed; - } - /** * Append any Standalone js to coreResourcesList. * @@ -512,9 +546,9 @@ public function appendCoreResources(\Civi\Core\Event\GenericHookEvent $e) { * @inheritDoc */ public function getTimeZoneString() { - $userId = Security::singleton()->getLoggedInUfID(); + $userId = $this->getLoggedInUfID(); if ($userId) { - $user = Security::singleton()->loadUserByID($userId); + $user = $this->getUserById($userId); if ($user && !empty($user['timezone'])) { return $user['timezone']; } @@ -524,19 +558,10 @@ public function getTimeZoneString() { /** * @inheritDoc + * @todo implement language negotiation for Standalone? */ public function languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLanguagePart = FALSE) { - if (empty($url)) { - return $url; - } - - // This method is called early in the boot process. - // Check if the extensions are available yet as our implementation requires Standaloneusers. - // Debugging note: calling Civi::log() methods here creates a nasty crash. - if (!class_exists(\Civi\Standalone\Security::class)) { - return $url; - } - return Security::singleton()->languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLanguagePart = FALSE); + return $url; } /** @@ -544,12 +569,12 @@ public function languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLan * @return array */ public function getCMSPermissionsUrlParams() { - return Security::singleton()->getCMSPermissionsUrlParams(); + return ['ufAccessURL' => '/civicrm/admin/roles']; } public function permissionDenied() { // If not logged in, they need to. - if (CRM_Core_Session::singleton()->get('ufID')) { + if ($this->isUserLoggedIn()) { // They are logged in; they're just not allowed this page. CRM_Core_Error::statusBounce(ts("Access denied"), CRM_Utils_System::url('civicrm')); } @@ -563,20 +588,22 @@ public function permissionDenied() { return $loginPage->run(); } - throw new CRM_Core_Exception('Access denied. Standaloneusers extension not found'); + throw new CRM_Core_Exception('Access denied. Standaloneusers login page not found'); } } /** * Start a new session. + * + * Generally this uses the SessionHander provided by Standaloneusers + * extension - but we fallback to a default PHP session to: + * a) allow the installer to work (early in the Standalone install, we dont have Standaloneusers yet) + * b) avoid unhelpfully hard crash if the ExtensionSystem goes down (without the fallback, the crash + * here swallows whatever error is actually causing the crash) */ public function sessionStart() { - if (defined('CIVI_SETUP')) { - // during installation we can't use the session - // handler from the extension yet so we just - // use a default php session - // use a different cookie name to avoid any nasty clash - $session_cookie_name = 'SESSCIVISOINSTALL'; + if (!$this->isUserExtensionAvailable()) { + $session_cookie_name = 'SESSCIVISOFALLBACK'; } else { $session_handler = new SessionHandler(); @@ -640,17 +667,7 @@ public function postContainerBoot(): void { $sess = \CRM_Core_Session::singleton(); $sess->initialize(); - // We want to apply timezone for this session - // However - our implementation relies on checks against standaloneusers - // so we need a guard if this is called in install - // - // Doesn't the session handler started above also need standalonusers? - // Yes it does - but we put in some guards further into those functions - // to use a fake session instead for this install bit. - // Maybe they could get moved up here - if (class_exists(\Civi\Standalone\Security::class)) { - $this->setTimeZone(); - } + $this->setTimeZone(); } } diff --git a/ext/standaloneusers/CRM/Standaloneusers/Page/Login.php b/ext/standaloneusers/CRM/Standaloneusers/Page/Login.php index ea1adafaff6c..4a1c48501122 100644 --- a/ext/standaloneusers/CRM/Standaloneusers/Page/Login.php +++ b/ext/standaloneusers/CRM/Standaloneusers/Page/Login.php @@ -1,12 +1,10 @@ getLoggedInUfID(); - if (CRM_Core_Session::singleton()->get('ufID')) { + if (CRM_Core_Config::singleton()->userSystem->isUserLoggedIn()) { // Already logged in. CRM_Utils_System::redirect('/civicrm'); } @@ -26,7 +24,7 @@ public function run() { * Log out. */ public static function logout() { - Security::singleton()->logoutUser(); + CRM_Core_Config::singleton()->userSystem->logout(); // Dump them back on the log-IN page. CRM_Utils_System::redirect('/civicrm/login?justLoggedOut'); } diff --git a/ext/standaloneusers/Civi/Api4/Action/User/Login.php b/ext/standaloneusers/Civi/Api4/Action/User/Login.php index 04677299117f..5eb73ed350b5 100644 --- a/ext/standaloneusers/Civi/Api4/Action/User/Login.php +++ b/ext/standaloneusers/Civi/Api4/Action/User/Login.php @@ -153,8 +153,7 @@ protected function passwordCheck(Result $result) { } protected function loginUser(int $userID) { - $authx = new \Civi\Authx\Standalone(); - $authx->loginSession($userID); + _authx_uf()->loginSession($userID); } } diff --git a/ext/standaloneusers/Civi/Api4/Action/User/PasswordReset.php b/ext/standaloneusers/Civi/Api4/Action/User/PasswordReset.php index 6e71dd1cde22..b4c01b9cfcd7 100644 --- a/ext/standaloneusers/Civi/Api4/Action/User/PasswordReset.php +++ b/ext/standaloneusers/Civi/Api4/Action/User/PasswordReset.php @@ -39,8 +39,7 @@ public function _run(Result $result) { // todo: some minimum password quality check? // Only valid change here is password, for a known ID. - $security = Security::singleton(); - $userID = $security->checkPasswordResetToken($this->token); + $userID = Security::singleton()->checkPasswordResetToken($this->token); if (!$userID) { throw new API_Exception("Invalid token."); } diff --git a/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php b/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php index f99d6e3b01d0..547cc9535438 100644 --- a/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php +++ b/ext/standaloneusers/Civi/Api4/Action/User/WriteTrait.php @@ -75,15 +75,10 @@ protected function validateValues() { $loggedInUserID = \CRM_Utils_System::getLoggedInUfID() ?? FALSE; $hasAdminPermission = \CRM_Core_Permission::check(['cms:administer users']); $authenticatedAsLoggedInUser = FALSE; - $security = Security::singleton(); // Check that we have the logged-in-user's password. if ($this->actorPassword && $loggedInUserID) { - $storedHashedPassword = \Civi\Api4\User::get(FALSE) - ->addWhere('id', '=', $loggedInUserID) - ->addSelect('hashed_password') - ->execute() - ->single()['hashed_password']; - if (!$security->checkPassword($this->actorPassword, $storedHashedPassword)) { + $user = \CRM_Core_Config::singleton()->userSystem->getUserById($loggedInUserID); + if (!_authx_uf()->checkPassword($user['username'], $this->actorPassword)) { throw new UnauthorizedException("Incorrect password"); } $authenticatedAsLoggedInUser = TRUE; diff --git a/ext/standaloneusers/Civi/Authx/Standalone.php b/ext/standaloneusers/Civi/Authx/Standalone.php index 0fe4c66466b2..c2d0c944f818 100644 --- a/ext/standaloneusers/Civi/Authx/Standalone.php +++ b/ext/standaloneusers/Civi/Authx/Standalone.php @@ -19,33 +19,48 @@ class Standalone implements AuthxInterface { * @inheritDoc */ public function checkPassword(string $username, string $password) { - $security = Security::singleton(); - $user = $security->loadUserByName($username); - return $security->checkPassword($password, $user['hashed_password'] ?? '') ? $user['id'] : NULL; + return Security::singleton()->checkPassword($username, $password); } /** * @inheritDoc */ public function loginSession($userId) { - $user = Security::singleton()->loadUserByID($userId); - Security::singleton()->loginAuthenticatedUserRecord($user, TRUE); + $this->loginStateless($userId); + + $session = \CRM_Core_Session::singleton(); + $session->set('ufID', $userId); + + // Identify the contact + $user = \Civi\Api4\User::get(FALSE) + ->addWhere('id', '=', $userId) + ->execute() + ->single(); + + // Confusingly, Civi stores it's *Contact* ID as *userID* on the session. + $session->set('userID', $user['contact_id'] ?? NULL); + + if (!empty($user['language'])) { + $session->set('lcMessages', $user['language']); + } } /** * @inheritDoc */ public function logoutSession() { + global $loggedInUserId; + $loggedInUserId = NULL; \CRM_Core_Session::singleton()->reset(); - session_destroy(); + // session_destroy(); } /** * @inheritDoc */ public function loginStateless($userId) { - $user = Security::singleton()->loadUserByID($userId); - Security::singleton()->loginAuthenticatedUserRecord($user, FALSE); + global $loggedInUserId; + $loggedInUserId = $userId; } /** diff --git a/ext/standaloneusers/Civi/Standalone/Security.php b/ext/standaloneusers/Civi/Standalone/Security.php index 6f453246d03e..95f24e354741 100644 --- a/ext/standaloneusers/Civi/Standalone/Security.php +++ b/ext/standaloneusers/Civi/Standalone/Security.php @@ -2,18 +2,25 @@ namespace Civi\Standalone; use Civi\Crypto\Exception\CryptoException; -use CRM_Core_Session; use Civi; use Civi\Api4\User; use Civi\Api4\MessageTemplate; use CRM_Standaloneusers_WorkflowMessage_PasswordReset; /** - * This is a single home for security related functions for Civi Standalone. + * Security related functions for Standaloneusers. * - * Things may yet move around in the codebase; at the time of writing this helps - * keep core PRs to a minimum. + * This is closely coupled with CRM_Utils_System_Standalone + * Many functions there started life here when Standalone + * was being resurrected. * + * Some of the generic user functions have been moved back to the + * System class so that they are more permanently available. + * + * Things may yet move around in the codebase - particularly if + * alternative user extensions to Standaloneusers are developed as + * these would then need to share an interface with the System + * class */ class Security { @@ -53,7 +60,10 @@ public function checkPermission(string $permissionName, ?int $userID = NULL) { } // NULL means the current logged-in user - $userID ??= $this->getLoggedInUfID() ?? 0; + $userID = $userID ?? \CRM_Utils_System::getLoggedInUfID(); + + // now any falsey userid is equivalent to userID = 0 = anonymous user + $userID = $userID ?: 0; if (!isset(\Civi::$statics[__METHOD__][$userID])) { @@ -88,176 +98,39 @@ public function checkPermission(string $permissionName, ?int $userID = NULL) { } /** + * High level function to encrypt password using the site-default mechanism. */ - public function getUserIDFromUsername(string $username): ?int { - return \Civi\Api4\User::get(FALSE) - ->addWhere('username', '=', $username) - ->execute() - ->first()['id'] ?? NULL; + public function hashPassword(string $plaintext): string { + // For now, we just implement D7's but this should be configurable. + // Sites should be able to move from one password hashing algo to another + // e.g. if a vulnerability is discovered. + $algo = new \Civi\Standalone\PasswordAlgorithms\Drupal7(); + return $algo->hashPassword($plaintext); } /** - * Load an active user by username. - * - * @return array|bool FALSE if not found. + * Standaloneusers implementation of AuthxInterface::checkPassword + * @see \Civi\Authx\Standalone */ - public function loadUserByName(string $username) { + public function checkPassword(string $username, string $plaintextPassword): ?int { $user = \Civi\Api4\User::get(FALSE) ->addWhere('username', '=', $username) ->addWhere('is_active', '=', TRUE) - ->execute()->first() ?? []; - if ($user) { - return $user; - } - return FALSE; - } - - /** - * Load an active user by internal user ID. - * - * @return array|bool FALSE if not found. - */ - public function loadUserByID(int $userID) { - $user = \Civi\Api4\User::get(FALSE) - ->addWhere('id', '=', $userID) - ->addWhere('is_active', '=', TRUE) - ->execute()->first() ?? []; - if ($user) { - return $user; - } - return FALSE; - } - - /** - * - */ - public function logoutUser() { - // This is the same call as in CRM_Authx_Page_AJAX::logout() - _authx_uf()->logoutSession(); - } - - /** - * Create a user in the CMS. - * - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - * - * @param array $params keys: - * - 'cms_name' - * - 'cms_pass' plaintext password - * - 'notify' boolean - * @param string $emailParam - * Name of the $param which contains the email address. - * - * @return int|bool - * uid if user was created, false otherwise - */ - public function createUser(&$params, $emailParam) { - try { - $email = $params[$emailParam]; - $userID = User::create(FALSE) - ->addValue('username', $params['cms_name']) - ->addValue('uf_name', $email) - ->addValue('password', $params['cms_pass']) - ->addValue('contact_id', $params['contact_id'] ?? NULL) - // ->addValue('uf_id', 0) // does not work without this. - ->execute()->single()['id']; - } - catch (\Exception $e) { - \Civi::log()->warning("Failed to create user '$email': " . $e->getMessage()); - return FALSE; - } - - // @todo This next line is what Drupal does, but it's unclear why. - // I think it assumes we want to be logged in as this contact, and as there's no uf match, it's not in civi. - // But I'm not sure if we are always becomming this user; I'm not sure waht calls this function. - // CRM_Core_Config::singleton()->inCiviCRM = FALSE; - - return (int) $userID; - } - - /** - * Update a user's email - * - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - */ - public function updateCMSName($ufID, $email) { - \Civi\Api4\User::update(FALSE) - ->addWhere('id', '=', $ufID) - ->addValue('uf_name', $email) - ->execute(); - } + ->addSelect('hashed_password', 'id') + ->execute() + ->first(); - /** - * Register the given user as the currently logged in user. - */ - public function loginAuthenticatedUserRecord(array $user, bool $withSession) { - global $loggedInUserId, $loggedInUser; - $loggedInUserId = $user['id']; - $loggedInUser = $user; - - if ($withSession) { - $session = \CRM_Core_Session::singleton(); - $session->set('ufID', $user['id']); - - // Identify the contact - $contactID = civicrm_api3('UFMatch', 'get', [ - 'sequential' => 1, - 'return' => ['contact_id'], - 'uf_id' => $user['id'], - ])['values'][0]['contact_id'] ?? NULL; - // Confusingly, Civi stores it's *Contact* ID as *userID* on the session. - $session->set('userID', $contactID); - $this->applyLocaleFromUser($user); + if ($user && $this->checkHashedPassword($plaintextPassword, $user['hashed_password'])) { + return $user['id']; } - } - - /** - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - */ - public function isUserLoggedIn(): bool { - return !empty($this->getLoggedInUfID()); - } - - /** - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - */ - public function getLoggedInUfID(): ?int { - $authX = new \Civi\Authx\Standalone(); - return $authX->getCurrentUserId(); - } - - /** - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - */ - public function languageNegotiationURL($url, $addLanguagePart = TRUE, $removeLanguagePart = FALSE) { - // @todo - return $url; - } - - /** - * This is the (perhaps temporary location for) the implementation of CRM_Utils_System_Standalone method. - * Return the CMS-specific url for its permissions page - * @return array - */ - public function getCMSPermissionsUrlParams() { - return ['ufAccessURL' => '/civicrm/admin/roles']; - } - - /** - * High level function to encrypt password using the site-default mechanism. - */ - public function hashPassword(string $plaintext): string { - // For now, we just implement D7's but this should be configurable. - // Sites should be able to move from one password hashing algo to another - // e.g. if a vulnerability is discovered. - $algo = new \Civi\Standalone\PasswordAlgorithms\Drupal7(); - return $algo->hashPassword($plaintext); + return NULL; } /** * Check whether a password matches a hashed version. + * @return bool */ - public function checkPassword(string $plaintextPassword, string $storedHashedPassword): bool { + protected function checkHashedPassword(string $plaintextPassword, string $storedHashedPassword): bool { if (preg_match('@^\$S\$[A-Za-z./0-9]{52}$@', $storedHashedPassword)) { // Looks like a default D7 password. @@ -410,17 +283,4 @@ public function preparePasswordResetWorkflow(array $user, string $token): ?CRM_S return $workflowMessage; } - /** - * Applies the locale from the user record. - * - * @param array $user - * @return void - */ - public function applyLocaleFromUser(array $user) { - $session = CRM_Core_Session::singleton(); - if (!empty($user['language'])) { - $session->set('lcMessages', $user['language']); - } - } - } diff --git a/ext/standaloneusers/tests/phpunit/Civi/Api4/Action/UserTest.php b/ext/standaloneusers/tests/phpunit/Civi/Api4/Action/UserTest.php index 797783d6dfda..487d902c8937 100644 --- a/ext/standaloneusers/tests/phpunit/Civi/Api4/Action/UserTest.php +++ b/ext/standaloneusers/tests/phpunit/Civi/Api4/Action/UserTest.php @@ -8,10 +8,9 @@ use Civi\Api4\Role; use Civi\Api4\UserRole; use Civi\Api4\Contact; -use Civi\Standalone\Security; /** - * FIXME - Add test description. + * Test the Standaloneusers User Api4 actions * * Tips: * - With HookInterface, you may implement CiviCRM hooks directly in the test class. @@ -92,41 +91,21 @@ public function setUp():void { } } - /** - * Note I thought I could use \Civi\Authx\Standalone::logoutSession() - * but it calls session_destroy which messes up future tests. - * - * Not sure if there is a generic logout without session destroy. - * - */ public function ensureLoggedOut() { - global $loggedInUserId, $loggedInUser; - - if (\CRM_Utils_System::getLoggedInUfID()) { - \CRM_Core_Session::singleton()->reset(); - $loggedInUser = $loggedInUserId = NULL; - } + \CRM_Utils_System::logout(); } public function tearDown():void { - $this->deleteStuffWeMade(); + // only tear down if we set up + if (CIVICRM_UF === 'Standalone') { + $this->ensureLoggedOut(); + $this->deleteStuffWeMade(); + } parent::tearDown(); } protected function loginUser($userID) { - $security = Security::singleton(); - $user = \Civi\Api4\User::get(FALSE) - ->addWhere('id', '=', $userID) - ->execute()->first(); - - $contactID = civicrm_api3('UFMatch', 'get', [ - 'sequential' => 1, - 'return' => ['contact_id'], - 'uf_id' => $user['id'], - ])['values'][0]['contact_id'] ?? NULL; - $this->assertNotNull($contactID); - /** @var \Civi\Standalone\Security $security */ - $security->loginAuthenticatedUserRecord($user, FALSE); + _authx_uf()->loginSession($userID); } /** diff --git a/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php b/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php index e08e65d63a5a..d91db2d8ef58 100644 --- a/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php +++ b/ext/standaloneusers/tests/phpunit/Civi/Standalone/SecurityTest.php @@ -6,7 +6,7 @@ use Civi\Api4\User; /** - * FIXME - Add test description. + * Test Security flows in Standalone * * Tips: * - With HookInterface, you may implement CiviCRM hooks directly in the test class. @@ -52,7 +52,6 @@ public function tearDown():void { } protected function loginUser($userID) { - $security = Security::singleton(); $user = \Civi\Api4\User::get(FALSE) ->addWhere('id', '=', $userID) ->execute()->first(); @@ -63,8 +62,8 @@ protected function loginUser($userID) { 'uf_id' => $user['id'], ])['values'][0]['contact_id'] ?? NULL; $this->assertNotNull($contactID); - /** @var \Civi\Standalone\Security $security */ - $security->loginAuthenticatedUserRecord($user, FALSE); + + \CRM_Core_Config::singleton()->userSystem->loadUser($user['username']); } public function testCheckPassword():void { @@ -75,8 +74,8 @@ public function testCheckPassword():void { ->execute()->single(); // Test that the password can be checked ok. - $this->assertTrue($security->checkPassword('secret1', $user['hashed_password'])); - $this->assertFalse($security->checkPassword('some other password', $user['hashed_password'])); + $this->assertTrue((bool) $security->checkPassword($user['username'], 'secret1')); + $this->assertFalse((bool) $security->checkPassword($user['username'], 'some other password')); } public function testPerms() { @@ -209,7 +208,7 @@ public function testForgottenPassword() { $this->assertEquals(1, $result['success']); $user = User::get(FALSE)->addWhere('id', '=', $userID)->execute()->single(); - $this->assertTrue($security->checkPassword('fingersCrossed', $user['hashed_password'])); + $this->assertTrue((bool) $security->checkPassword($user['username'], 'fingersCrossed')); // Should not work a 2nd time with same token. try { @@ -238,12 +237,6 @@ public function testForgottenPassword() { $this->assertNull($security->checkPasswordResetToken($token)); } - public function testGetUserIDFromUsername() { - [$contactID, $adminUserID, $security] = $this->createFixtureContactAndUser(); - $this->assertEquals($adminUserID, $security->getUserIDFromUsername('user_one'), 'Should return admin user ID'); - $this->assertNull($security->getUserIDFromUsername('user_unknown'), 'Should return NULL for non-existent user'); - } - protected function deleteStuffWeMade() { User::delete(FALSE)->addWhere('username', '=', 'testuser1')->execute(); }