namespace Civi\Pipe;
+use Civi\Authx\AuthxException;
+
/**
* Collection of methods to expose to the pipe session. Any public method will be accessible.
*/
class PublicMethods {
+ /**
+ * How should API errors be reported?
+ *
+ * @var string
+ * - 'array': Traditional array format from civicrm_api(). Maximizes consistency of error data.
+ * - 'exception': Converted to an exception. Somewhat lossy. Improves out-of-box DX on stricter JSON-RPC clients.
+ */
+ protected $apiError = 'exception';
+
+ /**
+ * 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.
*
- * @param $session
+ * @param \Civi\Pipe\PipeSession $session
* @param array $request
* Tuple: [$entity, $action, $params]
* @return array|\Civi\Api4\Generic\Result|int
*/
- public function api3($session, $request) {
- $request[2] = array_merge(['version' => 3, 'check_permissions' => TRUE], $request[2] ?? []);
- return civicrm_api(...$request);
+ public function api3(PipeSession $session, array $request) {
+ $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);
+
+ case 'exception':
+ return civicrm_api3(...$request);
+
+ default:
+ throw new \CRM_Core_Exception("Invalid API error-handling mode: $this->apiError");
+ }
}
/**
* Send a request to APIv4.
*
- * @param $session
+ * @param \Civi\Pipe\PipeSession $session
* @param array $request
* Tuple: [$entity, $action, $params]
* @return array|\Civi\Api4\Generic\Result|int
*/
- public function api4($session, $request) {
- $request[2] = array_merge(['version' => 4, 'checkPermissions' => TRUE], $request[2] ?? []);
- return civicrm_api(...$request);
+ public function api4(PipeSession $session, array $request) {
+ $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);
+
+ case 'exception':
+ return civicrm_api4(...$request);
+
+ default:
+ throw new \CRM_Core_Exception("Invalid API error-handling mode: $this->apiError");
+ }
}
/**
* Simple test; send/receive a fragment of data.
*
- * @param $session
- * @param mixed $request
- * @return mixed
+ * @param \Civi\Pipe\PipeSession $session
+ * @param array $request
+ * @return array
*/
- public function echo($session, $request) {
+ public function echo(PipeSession $session, array $request) {
return $request;
}
/**
* Set active user.
*
- * @param $session
- * @param array{contactId: int, userId: int, user: string} $request
+ * @param \Civi\Pipe\PipeSession $session
+ * @param array{contactId: int, userId: int, user: string, cred: string} $request
* @return array|\Civi\Api4\Generic\Result|int
*/
- public function login($session, $request) {
+ public function login(PipeSession $session, array $request) {
if (!function_exists('authx_login')) {
- throw new \CRM_Core_Exception("Cannot authenticate. Authx is not configured.");
+ 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(['flow' => 'script', 'principal' => $principal]));
+ }
+ 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' => 'pipe', 'cred' => $request['cred']])) {
+ return $redact(\CRM_Core_Session::singleton()->get('authx'));
+ }
+ }
+
+ throw new AuthxException('Cannot authenticate. Must specify principal/credentials.');
}
/**
* Set ephemeral session options.
*
- * @param $session
- * @param array{maxLines: int, responsePrefix: int} $request
+ * @param \Civi\Pipe\PipeSession $session
+ * @param array{bufferSize: int, responsePrefix: int} $request
* Any updates to perform. May be empty/omitted.
- * @return array{maxLines: int, responsePrefix: int}
+ * @return array{bufferSize: int, responsePrefix: int}
* List of updated options.
* If the list of updates was empty, then return all options.
*/
- public function options($session, $request) {
- $map = [
+ public function options(PipeSession $session, array $request) {
+ $storageMap = [
+ 'apiCheckPermissions' => $this,
+ 'apiError' => $this,
+ 'bufferSize' => $session,
'responsePrefix' => $session,
- 'maxLine' => $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)}();
+ }
+ else {
+ return $storage->{$name};
+ }
+ };
+
+ $set = function($storage, $name, $value) use ($get) {
+ if (method_exists($storage, 'set' . ucfirst($name))) {
+ $storage->{'set' . ucfirst($name)}($value);
+ }
+ else {
+ $storage->{$name} = $value;
+ }
+ return $get($storage, $name);
+ };
+
$result = [];
if (!empty($request)) {
- foreach ($request as $option => $value) {
- if (isset($map[$option])) {
- $storage = $map[$option];
- $storage->{'set' . ucfirst($option)}($value);
- $result[$option] = $storage->{'get' . ucfirst($option)}();
+ foreach ($request as $name => $value) {
+ if (isset($storageMap[$name])) {
+ $result[$name] = $set($storageMap[$name], $name, $value);
}
}
}
else {
- foreach ($map as $option => $storage) {
- $result[$option] = $storage->{'get' . ucfirst($option)}();
+ foreach ($storageMap as $name => $storage) {
+ $result[$name] = $get($storage, $name);
}
}
return $result;
}
+ private function isCheckPermissions(array $params, string $field) {
+ return isset($params[$field]) ? $params[$field] : $this->apiCheckPermissions;
+ }
+
}