Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add authx_login() API for backend script authentication (alt) #22292

Merged
merged 4 commits into from
Dec 24, 2021

Conversation

totten
Copy link
Member

@totten totten commented Dec 22, 2021

Overview

Adds a function to login as a user. It is cross-platform; accepts contact ID, user ID, or username; and it updates relevant services (eg Civi session; Drupal $user; MySQL @civicrm_user_id).

(This is a dependency for https://lab.civicrm.org/dev/core/-/issues/1304. @demeritcowboy this replaces #22239; it generally the same functionality, but some commits have been squashed, and some signatures have changed. In particular, authx_login() accepts more inputs - cred: string or principal: array combined with flow: string and/or useSession: bool. Ex: Setting flow=>script means that it will use the setting authx_script_user to decide whether user-accounts are required.)

Before

Not available

After

// Signature
function authx_login(array $params): array;

// Examples
authx_login(['principal' => ['contactId' => 202]]);
authx_login(['principal' => ['userId' => 1]]);
authx_login(['principal' => ['user' => 'admin']]);
authx_login(['cred' => 'Basic dXNlcjpwYXNz']);
authx_login(['cred' => 'Bearer dXNlcjpwYXNz']);

authx_login(['flow' => 'foo', 'useSession' => TRUE, 'principal' => ['user' => 'admin']]);
authx_login(['flow' => 'foo', 'useSession' => TRUE, 'cred' => 'Bearer dXNlcjpwYXNz']);

This shares much of its implementation with the login processes used by authx for JWT+ApiKey+Password authentication.

Like the JWT/key/password implementations, this has cross-platform E2E testing.

Technical Details

The new use-case is like this: you have some background worker processes. These processes need to execute some tasks on behalf of a user. This is similar to authx's existing login functionality in some ways (eg you want it to be portable; to integrate with login/session mechanics in the CMS; and to work with a range of user-accounts); but it also differs in an important way (eg there is no authentication credential presented by a user; the decision is made by a background agent).

The main changes here:

  • Update Civi\Authx\Authenticator::auth() so that it can accept either (a) unvalidated credentials (string $cred; e.g. username-password/JWT/API-key) or (b) validated array $principal (e.g. validated int $contactId or int $userId).
  • Add a wrapper method authx_login(). This calls Civi\Authx\Authenticator::auth() with some defaults that are more useful for custom scripts.

A quick way to see it in action is with cv, drush, or wp-cli. In each of these commands, we start Civi in different ways (cv ev, drush ev, etc) and try to login.

#### Valid examples (for standard demo data; cv)

cv ev 'authx_login(["principal"=>["contactId"=>202]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

cv ev 'authx_login(["principal"=>["userId"=>1]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

cv ev 'authx_login(["principal"=>["user"=>"admin"]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

#### Valid examples (for standard demo data; drush-d7, drush-d8, wp-cli)

drush ev 'civicrm_initialize(); authx_login(["principal"=>["contactId"=>202]],0); print_r(civicrm_api3("Contact","get",["id"=>"user_contact_id"]));'

drush ev '\Drupal::service("civicrm")->initialize(); authx_login(["principal"=>["contactId"=>202]],0); print_r(civicrm_api3("Contact","get",["id"=>"user_contact_id"]));'

wp eval 'civicrm_initialize(); authx_login(["principal"=>["contactId"=>202]],0); print_r(civicrm_api3("Contact","get",["id"=>"user_contact_id"]));'

#### Invalid examples (for standard demo data; cv)

cv ev 'authx_login(["principal"=>["contactId"=>99999]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  [Civi\Authx\AuthxException]
  Contact ID 99999 is invalid

cv ev 'authx_login(["principal"=>["userId"=>99999]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  [Civi\Authx\AuthxException]
  Cannot login. Failed to determine contact ID.

cv ev 'authx_login(["principal"=>["user"=>"nonexistent"]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  [Civi\Authx\AuthxException]
  Must specify principal with valid user, userId, or contactId

cv ev -U admin 'authx_login(["principal"=>["user"=>"demo"]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  [Civi\Authx\AuthxException]
  Cannot login. Session already active.

#### Invalid examples (for standard demo data; drush-d7, drush-d8)

drush ev 'civicrm_initialize(); authx_login(["principal"=>["contactId"=>99999]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  CRM_Core_Exception: [0: Contact ID 99999 is invalid                                                              [error]

drush -u admin ev 'civicrm_initialize(); authx_login(["principal"=>["user"=>"demo"]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  CRM_Core_Exception: [0: Cannot login. Session already active.                                                    [error]

drush ev '\Drupal::service("civicrm")->initialize(); authx_login(["principal"=>["contactId"=>99999]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  CRM_Core_Exception: [0: Contact ID 99999 is invalid                                                                      [error]

drush -u admin ev '\Drupal::service("civicrm")->initialize(); authx_login(["principal"=>["user"=>"demo"]],0); return civicrm_api3("Contact","get",["id"=>"user_contact_id"]);'

  CRM_Core_Exception: [0: Cannot login. Session already active.                                                                                                   [error]

(Note @demeritcowboy - I tested most of the above in drupal-clean w/php80+php74 and drupal8-clean w/php74. I couldn't reproduce the type error mentioned here. If you still get the error, then I'll need more precise description of the environment.)

There are some existing alternatives, but each has issues.

  • Use CRM_Utils_System::loadBootStrap(...) and pass username/password. However, in this function, the authentication and bootstrap processes are tightly coupled -- and some of its expectations are wonky.
  • Use CMS-specific APIs, eg \Drupal::service('account_switcher')->switchTo(...) or wp_set_current_user(). Obviously not portable.
  • Use cv --user, drush --user, or wp --user. However, these require you pick the target principal/user before launching the script.
  • Use CRM_Core_Config::singleton()->userSystem->loadUser(...). However, this only workswith a username (not contactId/userId); the incantation is very internal; and test coverage is unclear.

As mentioned in civicrm#22239 (comment), there can be an exception:

```
Error: Call to a member function id() on bool in ...\CRM\Utils\System\Drupal8.php on line 359 #0 ...\ext\authx\Civi\Authx\Authenticator.php(380)
```

The function signature specifies `@return int|null`. Various callers appear to expect this, and that seems to be how it behaves on D7/WP.
Before: If the authenticator encounters an error, it sends a response document.

After: The authenticator encoutners an error, it may either:

- (Default) Send a response document
- (Option) Emit an exception
@civibot
Copy link

civibot bot commented Dec 22, 2021

(Standard links)

@demeritcowboy
Copy link
Contributor

For the type error, I step-debugged and what happens is line 222 happens, so it never reaches line 252, so line 76 in authx.php returns null.

It's drupal 9.3, civi master, php 7.4. It's windows but that shouldn't matter here. There's no extensions (except the stock ones enabled by default and of course authx). I don't see anything unusual in civicrm.settings.php except maybe define('CIVICRM_CRED_KEYS', 'plain'); but I don't see where that would affect the above code flow. It thinks it's already logged in (which is true) so it never sets 'authx' in the session.

cv ev --user=admin "authx_login(['principal' => ['userId' => 1]], false);", where admin is the cms username for cms user 1.

@totten
Copy link
Member Author

totten commented Dec 23, 2021

@demeritcowboy Aaaah, ok. That makes sense. I can reproduce it by choosing the same user for both login mechanisms (--user=... and principal=>...).

I guess that circumstance is not an error (expected post-condition is met) - but it's also not quite right (it's redundant to specify both --user and authx_login(); writing code which uses both means you may get exceptions when the data changes).

So I've pushed up a patch 322c4d0 which resolves the TypeError; instead, it emits a warning and constructs a placeholder array to describe the already-logged-in user.

@demeritcowboy
Copy link
Contributor

Looks good!

I also tried a couple other things just to see if I understood, e.g.

  • I set 'authx_script_cred' to include 'pass' and then was able to log in with 'cred' => 'Basic base64user:pass'.
  • I set 'authx_script_user' to 'require' and then it did require me to use a contactId with a cms account.

Although even with 'require', it did still let me "log in" as a user who was blocked in the cms which felt wrong. But maybe it worked that way before too in other flows so not going to hold this up on that.

@demeritcowboy demeritcowboy merged commit 14418d2 into civicrm:master Dec 24, 2021
@totten totten deleted the master-authxlogin-2 branch December 24, 2021 21:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants