From 5e13f3883f8e33fcf4722af265b1db9ae5225506 Mon Sep 17 00:00:00 2001 From: Tim Otten Date: Tue, 14 Dec 2021 22:11:43 -0800 Subject: [PATCH] Civi::pipe - Introduce `$trusted` flag and `$apiCheckPermissions` option * `bool $trusted` is an immutable property of the session that determines whether you can request special operations like: * Bypassing API permissions * Setting the active user without credentials * `bool $apiCheckPermissions` is a convenience option for `api3` and `api4`. This toggles the default value of `checkPermissions` / `check_permissions` on API calls. (It is only applicable to trusted sessions.) --- Civi.php | 5 +- Civi/Pipe/PipeSession.php | 25 +++++++++ Civi/Pipe/PublicMethods.php | 51 +++++++++++++++++-- .../phpunit/Civi/Pipe/JsonRpcSessionTest.php | 3 +- 4 files changed, 78 insertions(+), 6 deletions(-) diff --git a/Civi.php b/Civi.php index b1e3e44eab..828d58ecbd 100644 --- a/Civi.php +++ b/Civi.php @@ -120,7 +120,10 @@ class Civi { * @see \Civi\Pipe\BasicJsonSession */ public static function pipe(string $protocol = 'jsonrpc20'): void { - Civi::service('pipe.' . $protocol)->setIO(STDIN, STDOUT)->run(); + Civi::service('pipe.' . $protocol) + ->setIO(STDIN, STDOUT) + ->setTrusted(TRUE) + ->run(); } /** diff --git a/Civi/Pipe/PipeSession.php b/Civi/Pipe/PipeSession.php index 68d833b4ec..322942c95d 100644 --- a/Civi/Pipe/PipeSession.php +++ b/Civi/Pipe/PipeSession.php @@ -24,6 +24,11 @@ class PipeSession { */ protected $methods; + /** + * @var bool|null + */ + protected $trusted; + /** * @inheritDoc */ @@ -59,4 +64,24 @@ class PipeSession { return \json_encode($error); } + /** + * @param bool $trusted + * @return PipeSession + */ + public function setTrusted(bool $trusted): PipeSession { + if ($this->trusted !== NULL && $this->trusted !== $trusted) { + throw new \CRM_Core_Exception('Cannot modify PipeSession::$trusted after initialization'); + } + $this->trusted = $trusted; + return $this; + } + + /** + * @return bool + */ + public function isTrusted(): bool { + // If this gets called when the value is NULL, then you are doing it wrong. + return $this->trusted; + } + } diff --git a/Civi/Pipe/PublicMethods.php b/Civi/Pipe/PublicMethods.php index 4f49eb0cbe..f64c743796 100644 --- a/Civi/Pipe/PublicMethods.php +++ b/Civi/Pipe/PublicMethods.php @@ -11,6 +11,8 @@ namespace Civi\Pipe; +use Civi\Authx\AuthxException; + /** * Collection of methods to expose to the pipe session. Any public method will be accessible. */ @@ -25,6 +27,15 @@ class PublicMethods { */ protected $apiError = 'array'; + /** + * Should API calls use permission checks? + * + * Note: This property is only consulted on trusted connections. It is ignored on untrusted connections. + * + * @var bool + */ + protected $apiCheckPermissions = TRUE; + /** * Send a request to APIv3. * @@ -34,7 +45,9 @@ class PublicMethods { * @return array|\Civi\Api4\Generic\Result|int */ public function api3($session, $request) { - $request[2] = array_merge(['version' => 3, 'check_permissions' => TRUE], $request[2] ?? []); + $request[2] = array_merge($request[2] ?? [], ['version' => 3]); + $request[2]['check_permissions'] = !$session->isTrusted() || $this->isCheckPermissions($request[2], 'check_permissions'); + // ^^ Untrusted sessions MUST check perms. All sessions DEFAULT to checking perms. Trusted sessions MAY disable perms. switch ($this->apiError) { case 'array': return civicrm_api(...$request); @@ -56,7 +69,9 @@ class PublicMethods { * @return array|\Civi\Api4\Generic\Result|int */ public function api4($session, $request) { - $request[2] = array_merge(['version' => 4, 'checkPermissions' => TRUE], $request[2] ?? []); + $request[2] = array_merge($request[2] ?? [], ['version' => 4]); + $request[2]['checkPermissions'] = !$session->isTrusted() || $this->isCheckPermissions($request[2], 'checkPermissions'); + // ^^ Untrusted sessions MUST check perms. All sessions DEFAULT to checking perms. Trusted sessions MAY disable perms. switch ($this->apiError) { case 'array': return civicrm_api(...$request); @@ -91,8 +106,27 @@ class PublicMethods { if (!function_exists('authx_login')) { throw new \CRM_Core_Exception("Cannot authenticate. Authx is not configured."); } - $auth = authx_login($request, FALSE /* Pipe sessions do not need cookies or DB */); - return \CRM_Utils_Array::subset($auth, ['contactId', 'userId']); + + $redact = function(?array $authx) { + return $authx ? \CRM_Utils_Array::subset($authx, ['contactId', 'userId']) : FALSE; + }; + + $principal = \CRM_Utils_Array::subset($request, ['contactId', 'userId', 'user']); + if ($principal && $session->isTrusted()) { + return $redact(authx_login($request, FALSE /* Pipe sessions do not need cookies or DB */)); + } + elseif ($principal && !$session->isTrusted()) { + throw new AuthxException("Session is not trusted."); + } + elseif (isset($request['cred'])) { + $authn = new \Civi\Authx\Authenticator(); + $authn->setRejectMode('exception'); + if ($authn->auth(NULL, ['flow' => 'xheader', 'cred' => $request['cred']])) { + return $redact(\CRM_Core_Session::singleton()->get("authx")); + } + } + + throw new AuthxException("Cannot authenticate. Must specify principal/credentials."); } /** @@ -107,11 +141,16 @@ class PublicMethods { */ public function options($session, $request) { $storageMap = [ + 'apiCheckPermissions' => $this, 'apiError' => $this, 'bufferSize' => $session, 'responsePrefix' => $session, ]; + if (!$session->isTrusted() && array_key_exists('apiCheckPermissions', $request)) { + unset($request['apiCheckPermissions']); + } + $get = function($storage, $name) { if (method_exists($storage, 'get' . ucfirst($name))) { return $storage->{'get' . ucfirst($name)}(); @@ -147,4 +186,8 @@ class PublicMethods { return $result; } + private function isCheckPermissions(array $params, string $field) { + return isset($params[$field]) ? $params[$field] : $this->apiCheckPermissions; + } + } diff --git a/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php b/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php index df9e2c7d22..7ed3218e00 100644 --- a/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php +++ b/tests/phpunit/Civi/Pipe/JsonRpcSessionTest.php @@ -26,6 +26,7 @@ class JsonRpcSessionTest extends \CiviUnitTestCase { $this->input = fopen('php://memory', 'w'); $this->output = fopen('php://memory', 'w'); $this->server = new PipeSession($this->input, $this->output); + $this->server->setTrusted(TRUE); } protected function tearDown(): void { @@ -91,7 +92,7 @@ class JsonRpcSessionTest extends \CiviUnitTestCase { public function testControl() { $this->assertRequestResponse([ - '{"jsonrpc":"2.0","id":"c","method":"options"}' => '{"jsonrpc":"2.0","result":{"apiError":"array","bufferSize":524288,"responsePrefix":null},"id":"c"}', + '{"jsonrpc":"2.0","id":"c","method":"options"}' => '{"jsonrpc":"2.0","result":{"apiCheckPermissions":true,"apiError":"array","bufferSize":524288,"responsePrefix":null},"id":"c"}', '{"jsonrpc":"2.0","id":"c","method":"options","params":{"responsePrefix":"ZZ"}}' => 'ZZ{"jsonrpc":"2.0","result":{"responsePrefix":"ZZ"},"id":"c"}', '{"jsonrpc":"2.0","id":"c","method": "echo","params":[123]}' => 'ZZ{"jsonrpc":"2.0","result":[123],"id":"c"}', ]); -- 2.25.1