Port search API search more than one fields (#14646)

* Fix port search columns

* Port search API search more than one fields
Fixup port APIs
Change validate_column_list api helper to throw a renderable exception on error and return the valid columns
DeviceCache::get() can handle a bigger range of input

* whitespace

* Refactor exceptions a bit

* change throws type to be more generic

* Lint fixes
This commit is contained in:
Tony Murray 2022-11-18 16:27:56 -06:00 committed by GitHub
parent e851c9abd4
commit 752bbc1531
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 177 additions and 103 deletions

View File

@ -0,0 +1,47 @@
<?php
/**
* ApiException.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*
* @link https://www.librenms.org
*
* @copyright 2019 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Exceptions;
class ApiClientException extends \Exception
{
/** @var array */
private $output;
/**
* @param string $message
* @param array $output
*/
public function __construct($message = '', $output = [])
{
parent::__construct($message, 0, null);
$this->output = $output;
}
public function getOutput(): array
{
return $this->output;
}
}

View File

@ -1,5 +1,5 @@
<?php <?php
/** /*
* ApiException.php * ApiException.php
* *
* -Description- * -Description-
@ -15,28 +15,40 @@
* GNU General Public License for more details. * GNU General Public License for more details.
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
* @link https://www.librenms.org * @package LibreNMS
* * @link http://librenms.org
* @copyright 2019 Tony Murray * @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com> * @author Tony Murray <murraytony@gmail.com>
*/ */
namespace LibreNMS\Exceptions; namespace LibreNMS\Exceptions;
use Illuminate\Http\JsonResponse;
class ApiException extends \Exception class ApiException extends \Exception
{ {
private $output; /**
* @param string $message
public function __construct($message = '', $output = []) * @param int $code
* @param \Throwable|null $previous
*/
public function __construct($message = '', $code = 400, $previous = null)
{ {
parent::__construct($message, 0, null); parent::__construct($message, $code, $previous);
$this->output = $output;
} }
public function getOutput() /**
* Render the exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
*/
public function render($request): JsonResponse
{ {
return $this->output; return response()->json([
'status' => 'error',
'message' => $this->getMessage(),
], $this->getCode(), [], JSON_PRETTY_PRINT);
} }
} }

View File

@ -0,0 +1,35 @@
<?php
/*
* InvalidTableColumnException.php
*
* -Description-
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2022 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Exceptions;
class InvalidTableColumnException extends ApiException
{
public function __construct(
public readonly array $columns
) {
parent::__construct('Invalid columns: ' . join(',', $this->columns));
}
}

View File

@ -26,7 +26,7 @@
namespace App\ApiClients; namespace App\ApiClients;
use GuzzleHttp\Exception\RequestException; use GuzzleHttp\Exception\RequestException;
use LibreNMS\Exceptions\ApiException; use LibreNMS\Exceptions\ApiClientException;
class RipeApi extends BaseApi class RipeApi extends BaseApi
{ {
@ -38,7 +38,7 @@ class RipeApi extends BaseApi
/** /**
* Get whois info * Get whois info
* *
* @throws ApiException * @throws ApiClientException
*/ */
public function getWhois(string $resource): array public function getWhois(string $resource): array
{ {
@ -52,7 +52,7 @@ class RipeApi extends BaseApi
/** /**
* Get Abuse contact * Get Abuse contact
* *
* @throws ApiException * @throws ApiClientException
*/ */
public function getAbuseContact(string $resource): mixed public function getAbuseContact(string $resource): mixed
{ {
@ -64,7 +64,7 @@ class RipeApi extends BaseApi
} }
/** /**
* @throws ApiException * @throws ApiClientException
*/ */
private function makeApiCall(string $uri, array $options): mixed private function makeApiCall(string $uri, array $options): mixed
{ {
@ -73,13 +73,13 @@ class RipeApi extends BaseApi
if (isset($response_data['status']) && $response_data['status'] == 'ok') { if (isset($response_data['status']) && $response_data['status'] == 'ok') {
return $response_data; return $response_data;
} else { } else {
throw new ApiException('RIPE API call failed', $response_data); throw new ApiClientException('RIPE API call failed', $response_data);
} }
} catch (RequestException $e) { } catch (RequestException $e) {
$message = 'RIPE API call to ' . $e->getRequest()->getUri() . ' failed: '; $message = 'RIPE API call to ' . $e->getRequest()->getUri() . ' failed: ';
$message .= $e->getResponse()->getReasonPhrase() . ' ' . $e->getResponse()->getStatusCode(); $message .= $e->getResponse()->getReasonPhrase() . ' ' . $e->getResponse()->getStatusCode();
throw new ApiException( throw new ApiClientException(
$message, $message,
json_decode($e->getResponse()->getBody(), true) json_decode($e->getResponse()->getBody(), true)
); );

View File

@ -28,7 +28,7 @@ namespace App\Http\Controllers\Ajax;
use App\ApiClients\RipeApi; use App\ApiClients\RipeApi;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use LibreNMS\Exceptions\ApiException; use LibreNMS\Exceptions\ApiClientException;
class RipeNccApiController extends Controller class RipeNccApiController extends Controller
{ {
@ -50,7 +50,7 @@ class RipeNccApiController extends Controller
'message' => 'Queried', 'message' => 'Queried',
'output' => $output, 'output' => $output,
]); ]);
} catch (ApiException $e) { } catch (ApiClientException $e) {
$response = $e->getOutput(); $response = $e->getOutput();
$message = $e->getMessage(); $message = $e->getMessage();

View File

@ -87,13 +87,14 @@ Output:
} }
``` ```
### `search_ports in specific column` ### `search_ports in specific field(s)`
Specific search for ports matching the query. Specific search for ports matching the query.
Route: `/api/v0/ports/search/:field/:search` Route: `/api/v0/ports/search/:field/:search`
- search string to search in field specified by field - field: comma separated list of field(s) to search
- search: string to search in fields
Input: Input:

View File

@ -27,6 +27,7 @@ use App\Models\Sensor;
use App\Models\ServiceTemplate; use App\Models\ServiceTemplate;
use App\Models\UserPref; use App\Models\UserPref;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Routing\Router; use Illuminate\Routing\Router;
use Illuminate\Support\Arr; use Illuminate\Support\Arr;
@ -35,13 +36,14 @@ use Illuminate\Support\Str;
use LibreNMS\Alerting\QueryBuilderParser; use LibreNMS\Alerting\QueryBuilderParser;
use LibreNMS\Config; use LibreNMS\Config;
use LibreNMS\Exceptions\InvalidIpException; use LibreNMS\Exceptions\InvalidIpException;
use LibreNMS\Exceptions\InvalidTableColumnException;
use LibreNMS\Util\Graph; use LibreNMS\Util\Graph;
use LibreNMS\Util\IP; use LibreNMS\Util\IP;
use LibreNMS\Util\IPv4; use LibreNMS\Util\IPv4;
use LibreNMS\Util\Number; use LibreNMS\Util\Number;
use LibreNMS\Util\Rewrite; use LibreNMS\Util\Rewrite;
function api_success($result, $result_name, $message = null, $code = 200, $count = null, $extra = null) function api_success($result, $result_name, $message = null, $code = 200, $count = null, $extra = null): JsonResponse
{ {
if (isset($result) && ! isset($result_name)) { if (isset($result) && ! isset($result_name)) {
return api_error(500, 'Result name not specified'); return api_error(500, 'Result name not specified');
@ -68,12 +70,12 @@ function api_success($result, $result_name, $message = null, $code = 200, $count
return response()->json($output, $code, [], JSON_PRETTY_PRINT); return response()->json($output, $code, [], JSON_PRETTY_PRINT);
} // end api_success() } // end api_success()
function api_success_noresult($code, $message = null) function api_success_noresult($code, $message = null): JsonResponse
{ {
return api_success(null, null, $message, $code); return api_success(null, null, $message, $code);
} // end api_success_noresult } // end api_success_noresult
function api_error($statusCode, $message) function api_error($statusCode, $message): JsonResponse
{ {
return response()->json([ return response()->json([
'status' => 'error', 'status' => 'error',
@ -81,7 +83,7 @@ function api_error($statusCode, $message)
], $statusCode, [], JSON_PRETTY_PRINT); ], $statusCode, [], JSON_PRETTY_PRINT);
} // end api_error() } // end api_error()
function api_not_found() function api_not_found(): JsonResponse
{ {
return api_error(404, "This API route doesn't exist."); return api_error(404, "This API route doesn't exist.");
} }
@ -970,25 +972,16 @@ function list_available_wireless_graphs(Illuminate\Http\Request $request)
}); });
} }
function get_port_graphs(Illuminate\Http\Request $request) /**
* @throws \LibreNMS\Exceptions\ApiException
*/
function get_port_graphs(Illuminate\Http\Request $request): JsonResponse
{ {
$hostname = $request->route('hostname'); $device = DeviceCache::get($request->route('hostname'));
$columns = $request->get('columns', 'ifName'); $columns = validate_column_list($request->get('columns'), 'ports', ['ifName']);
if (($validate = validate_column_list($columns, 'ports')) !== true) { $ports = $device->ports()->isNotDeleted()->hasAccess(Auth::user())
return $validate; ->select($columns)->orderBy('ifIndex')->get();
}
// use hostname as device_id if it's all digits
$device_id = ctype_digit($hostname) ? $hostname : getidbyname($hostname);
$sql = '';
$params = [$device_id];
if (! device_permitted($device_id)) {
$sql = 'AND `port_id` IN (select `port_id` from `ports_perms` where `user_id` = ?)';
array_push($params, Auth::id());
}
$ports = dbFetchRows("SELECT $columns FROM `ports` WHERE `device_id` = ? AND `deleted` = '0' $sql ORDER BY `ifIndex`", $params);
return api_success($ports, 'ports'); return api_success($ports, 'ports');
} }
@ -1052,29 +1045,31 @@ function get_port_info(Illuminate\Http\Request $request)
}); });
} }
function search_ports(Illuminate\Http\Request $request) /**
* @throws \LibreNMS\Exceptions\ApiException
*/
function search_ports(Illuminate\Http\Request $request): JsonResponse
{ {
$columns = validate_column_list($request->get('columns'), 'ports', ['device_id', 'port_id', 'ifIndex', 'ifName']);
$field = $request->route('field'); $field = $request->route('field');
$search = $request->route('search'); $search = $request->route('search');
$columns = $request->get('columns');
if (($validate = validate_column_list($columns, 'ports')) !== true) { // if only field is set, swap values
return $validate; if (empty($search)) {
[$field, $search] = [$search, $field];
} }
$fields = validate_column_list($field, 'ports', ['ifAlias', 'ifDescr', 'ifName']);
$query = Port::hasAccess(Auth::user()) $ports = Port::hasAccess(Auth::user())
->select(['device_id', 'port_id', 'ifIndex', 'ifName', $columns]); ->isNotDeleted()
->where(function ($query) use ($fields, $search) {
if (isset($search)) { foreach ($fields as $field) {
$query->where($field, 'like', "%$search%"); $query->orWhere($field, 'like', "%$search%");
} else { }
$value = "%$field%"; })
$query->where('ifAlias', 'like', $value) ->select($columns)
->orWhere('ifDescr', 'like', $value) ->orderBy('ifName')
->orWhere('ifName', 'like', $value); ->get();
}
$ports = $query->orderBy('ifName')
->get();
if ($ports->isEmpty()) { if ($ports->isEmpty()) {
return api_error(404, 'No ports found'); return api_error(404, 'No ports found');
@ -1083,21 +1078,17 @@ function search_ports(Illuminate\Http\Request $request)
return api_success($ports, 'ports'); return api_success($ports, 'ports');
} }
function get_all_ports(Illuminate\Http\Request $request) /**
* @throws \LibreNMS\Exceptions\ApiException
*/
function get_all_ports(Illuminate\Http\Request $request): JsonResponse
{ {
$columns = $request->get('columns', 'port_id, ifName'); $columns = validate_column_list($request->get('columns'), 'ports', ['port_id', 'ifName']);
if (($validate = validate_column_list($columns, 'ports')) !== true) {
return $validate;
}
$params = []; $ports = Port::hasAccess(Auth::user())
$sql = ''; ->select($columns)
if (! Auth::user()->hasGlobalRead()) { ->isNotDeleted()
$sql = ' AND (device_id IN (SELECT device_id FROM devices_perms WHERE user_id = ?) OR port_id IN (SELECT port_id FROM ports_perms WHERE user_id = ?))'; ->get();
array_push($params, Auth::id());
array_push($params, Auth::id());
}
$ports = dbFetchRows("SELECT $columns FROM `ports` WHERE `deleted` = 0 $sql", $params);
return api_success($ports, 'ports'); return api_success($ports, 'ports');
} }
@ -1119,7 +1110,7 @@ function get_port_stack(Illuminate\Http\Request $request)
}); });
} }
function update_device_port_notes(Illuminate\Http\Request $request): \Illuminate\Http\JsonResponse function update_device_port_notes(Illuminate\Http\Request $request): JsonResponse
{ {
$portid = $request->route('portid'); $portid = $request->route('portid');
@ -1153,7 +1144,10 @@ function list_alert_rules(Illuminate\Http\Request $request)
return api_success($rules->toArray($request), 'rules'); return api_success($rules->toArray($request), 'rules');
} }
function list_alerts(Illuminate\Http\Request $request) /**
* @throws \LibreNMS\Exceptions\ApiException
*/
function list_alerts(Illuminate\Http\Request $request): JsonResponse
{ {
$id = $request->route('id'); $id = $request->route('id');
@ -1191,9 +1185,7 @@ function list_alerts(Illuminate\Http\Request $request)
if ($request->has('order')) { if ($request->has('order')) {
[$sort_column, $sort_order] = explode(' ', $request->get('order'), 2); [$sort_column, $sort_order] = explode(' ', $request->get('order'), 2);
if (($res = validate_column_list($sort_column, 'alerts')) !== true) { validate_column_list($sort_column, 'alerts');
return $res;
}
if (in_array($sort_order, ['asc', 'desc'])) { if (in_array($sort_order, ['asc', 'desc'])) {
$order = $request->get('order'); $order = $request->get('order');
} }
@ -2618,22 +2610,29 @@ function list_logs(Illuminate\Http\Request $request, Router $router)
return api_success($logs, 'logs', null, 200, null, ['total' => $count]); return api_success($logs, 'logs', null, 200, null, ['total' => $count]);
} }
function validate_column_list($columns, $tableName) /**
* @throws \LibreNMS\Exceptions\ApiException
*/
function validate_column_list(?string $columns, string $table, array $default = []): array
{ {
if ($columns == '') { // no user input, return default
return $default;
}
static $schema; static $schema;
if (is_null($schema)) { if (is_null($schema)) {
$schema = new \LibreNMS\DB\Schema(); $schema = new \LibreNMS\DB\Schema();
} }
$column_names = is_array($columns) ? $columns : explode(',', $columns); $column_names = is_array($columns) ? $columns : explode(',', $columns);
$valid_columns = $schema->getColumns($tableName); $valid_columns = $schema->getColumns($table);
$invalid_columns = array_diff(array_map('trim', $column_names), $valid_columns); $invalid_columns = array_diff(array_map('trim', $column_names), $valid_columns);
if (count($invalid_columns) > 0) { if (count($invalid_columns) > 0) {
return api_error(400, 'Invalid columns: ' . join(',', $invalid_columns)); throw new InvalidTableColumnException($invalid_columns);
} }
return true; return $column_names;
} }
function missing_fields($required_fields, $data) function missing_fields($required_fields, $data)

View File

@ -2430,26 +2430,6 @@ parameters:
count: 1 count: 1
path: LibreNMS/Device/YamlDiscovery.php path: LibreNMS/Device/YamlDiscovery.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:__construct\\(\\) has parameter \\$message with no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/ApiException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:__construct\\(\\) has parameter \\$output with no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/ApiException.php
-
message: "#^Method LibreNMS\\\\Exceptions\\\\ApiException\\:\\:getOutput\\(\\) has no return type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/ApiException.php
-
message: "#^Property LibreNMS\\\\Exceptions\\\\ApiException\\:\\:\\$output has no type specified\\.$#"
count: 1
path: LibreNMS/Exceptions/ApiException.php
- -
message: "#^Method LibreNMS\\\\Exceptions\\\\AuthenticationException\\:\\:__construct\\(\\) has parameter \\$code with no type specified\\.$#" message: "#^Method LibreNMS\\\\Exceptions\\\\AuthenticationException\\:\\:__construct\\(\\) has parameter \\$code with no type specified\\.$#"
count: 1 count: 1