Files
cacti/tests/Unit/ApiDenyByDefaultTest.php
Thomas Vincent a8e1afba73 Refuse to serve the REST scaffold unless it is switched on (#7716)
* security(api): refuse to serve the REST scaffold unless it is switched on

No route in it consults a realm, a session or a token, and only the missing
vendor tree was keeping the endpoints quiet.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* fix(api): refuse with an empty 404 and use tabs in the test

A JSON error body confirmed an API was behind the refusal, which is what the
404 was chosen to avoid.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: resolve includes through Cacti path constants

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* test: assert the include form the bootstrap can actually use

CACTI_PATH_INCLUDE is defined by global.php, so it does not exist at the
point that includes it. The file anchors on __DIR__ like its neighbours.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-08-21 12:30:21 -07:00

85 lines
3.8 KiB
PHP

<?php
/*
+-------------------------------------------------------------------------+
| Copyright (C) 2004-2026 The Cacti Group |
| |
| 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 2 |
| of the License, or (at your option) any later version. |
+-------------------------------------------------------------------------+
| Cacti: The Complete RRDtool-based Graphing Solution |
+-------------------------------------------------------------------------+
*/
/*
* issue#7715. Nothing under api/ authenticates or authorises anything: no route
* consults a realm, a session or a token. What kept the fourteen endpoints from
* answering was that api/vendor is not shipped, so Slim was missing and the
* entry point fatalled. That is packaging rather than a control, and it goes
* away the moment someone runs the composer install in api/README.md on a live
* host. The scaffold now refuses unless it is deliberately switched on.
*/
$api = file_get_contents(CACTI_PATH_BASE . '/api/public/index.php');
test('the entry point reads', function () use ($api) {
expect($api)->toBeString()->not->toBeEmpty();
});
test('no route consults a realm, a session or a token', function () use ($api) {
$db = file_get_contents(CACTI_PATH_BASE . '/api/include/db_functions.php');
/* If this ever stops being true the gate below can be reconsidered; while it
holds, serving without the gate means serving to anyone. */
foreach (['is_realm_allowed', 'sess_user_id', 'Bearer'] as $needle) {
expect($api)->not->toContain($needle)
->and($db)->not->toContain($needle);
}
});
test('it refuses unless the setting says otherwise', function () use ($api) {
expect($api)->toContain("if (read_config_option('api_enabled') !== 'on') {")
->and($api)->toContain('http_response_code(404);');
/* an empty 404 reads like any other missing path; a JSON error body would
confirm an API is behind it */
expect($api)->not->toContain("print json_encode(['error' => 'Not found']);")
->and($api)->not->toContain("header('Content-Type: application/json');");
// the refusal must come before Slim is handed the routes
$gate = strpos($api, "read_config_option('api_enabled')");
$app = strpos($api, 'AppFactory::create()');
expect($gate)->not->toBeFalse()
->and($app)->not->toBeFalse()
->and($gate)->toBeLessThan($app);
});
test('error details are a development choice, not the default', function () use ($api) {
expect($api)->not->toContain('$app->addErrorMiddleware(true, true, true);')
->and($api)->toContain('$app->addErrorMiddleware($api_developer_mode, true, true);');
});
test('the bootstrap include is anchored to the file', function () use ($api) {
/* the other two includes beside it already were; this one resolved against
the working directory, so it only worked from api/public. It anchors on
__DIR__ like its neighbours: CACTI_PATH_INCLUDE is defined by global.php
itself, so it does not exist yet at the point of this include. */
expect($api)->not->toContain("include '../../include/global.php';")
->and($api)->toContain("include __DIR__ . '/../../include/global.php';");
});
test('the gate behaves as written for every setting value', function () {
$refuses = function (string $value) : bool {
return $value !== 'on';
};
// only the exact string opens it; absent, empty or anything else refuses
expect($refuses(''))->toBeTrue()
->and($refuses('off'))->toBeTrue()
->and($refuses('1'))->toBeTrue()
->and($refuses('On'))->toBeTrue()
->and($refuses('on'))->toBeFalse();
});