Skip to content

Commit a337bdf

Browse files
committed
add read restrictions
1 parent 45a8345 commit a337bdf

File tree

2 files changed

+94
-4
lines changed

2 files changed

+94
-4
lines changed

src/Glpi/Api/HL/Controller/SetupController.php

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ final class SetupController extends AbstractController
5252
{
5353
public static function getRawKnownSchemas(): array
5454
{
55+
global $DB;
56+
5557
return [
5658
'LDAPDirectory' => [
5759
'x-version-introduced' => '2.0',
@@ -119,6 +121,29 @@ public static function getRawKnownSchemas(): array
119121
'name' => ['type' => Doc\Schema::TYPE_STRING],
120122
'value' => ['type' => Doc\Schema::TYPE_STRING],
121123
],
124+
'x-rights-conditions' => [
125+
'read' => static function () use ($DB) {
126+
// Make a SQL request to get all config items so we can check which are undisclosed
127+
// We are using safe IDs rather than undisclosed IDs to avoid issues with concurrent modifications
128+
// We cannot reliably lock the table due to the fact that the DB connection here may differ from the one used to perform the actual read in the Search code
129+
$disclosed_ids = [];
130+
131+
$it = $DB->request([
132+
'SELECT' => ['id', 'context', 'name'],
133+
'FROM' => 'glpi_configs',
134+
]);
135+
$test_configs = [];
136+
foreach ($it as $row) {
137+
$test_configs[] = $row + ['value' => 'dummy'];
138+
}
139+
foreach ($test_configs as $f) {
140+
if (!self::isUndisclosedConfig($f['context'], $f['name'])) {
141+
$disclosed_ids[] = $f['id'];
142+
}
143+
}
144+
return ['WHERE' => ['_.id' => $disclosed_ids]];
145+
}
146+
]
122147
],
123148
];
124149
}
@@ -230,7 +255,7 @@ public function deleteItem(Request $request): Response
230255
return ResourceAccessor::deleteBySchema($this->getKnownSchema($itemtype, $this->getAPIVersion($request)), $request->getAttributes(), $request->getParameters());
231256
}
232257

233-
private function isUndisclosedConfig(string $context, string $name): bool
258+
private static function isUndisclosedConfig(string $context, string $name): bool
234259
{
235260
$f = ['context' => $context, 'name' => $name, 'value' => 'dummy'];
236261
Config::unsetUndisclosedFields($f);
@@ -254,7 +279,7 @@ public function setConfigValue(Request $request): Response
254279
$value = $request->getParameter('value');
255280
Config::setConfigurationValues($context, [$name => $value]);
256281
// Return the updated config
257-
if ($this->isUndisclosedConfig($context, $name)) {
282+
if (self::isUndisclosedConfig($context, $name)) {
258283
// If the field is undisclosed, only return a 204 to indicate success without revealing the value
259284
return new JSONResponse(null, 204);
260285
}
@@ -265,6 +290,27 @@ public function setConfigValue(Request $request): Response
265290
]);
266291
}
267292

293+
#[Route(path: '/Config', methods: ['GET'], middlewares: [ResultFormatterMiddleware::class])]
294+
#[RouteVersion(introduced: '2.1')]
295+
#[Doc\SearchRoute(schema_name: 'Config')]
296+
public function searchConfigValues(Request $request): Response
297+
{
298+
return ResourceAccessor::searchBySchema($this->getKnownSchema('Config', $this->getAPIVersion($request)), $request->getParameters());
299+
}
300+
301+
#[Route(path: '/Config/{context}', methods: ['GET'], requirements: [
302+
'context' => '\w+'
303+
], middlewares: [ResultFormatterMiddleware::class])]
304+
#[RouteVersion(introduced: '2.1')]
305+
#[Doc\SearchRoute(schema_name: 'Config')]
306+
public function searchConfigValuesByContext(Request $request): Response
307+
{
308+
$filters = $request->hasParameter('filter') ? $request->getParameter('filter') : '';
309+
$filters .= ';context==' . $request->getAttribute('context');
310+
$request->setParameter('filter', $filters);
311+
return ResourceAccessor::searchBySchema($this->getKnownSchema('Config', $this->getAPIVersion($request)), $request->getParameters());
312+
}
313+
268314
#[Route(path: '/Config/{context}/{name}', methods: ['GET'], requirements: [
269315
'context' => '\w+',
270316
'name' => '\w+',
@@ -280,7 +326,7 @@ public function getConfigValue(Request $request): Response
280326
if (!$config->getFromDBByCrit(['context' => $context, 'name' => $name,])) {
281327
return AbstractController::getNotFoundErrorResponse();
282328
}
283-
if ($this->isUndisclosedConfig($context, $name) || !$config->can($config->getID(), READ)) {
329+
if (self::isUndisclosedConfig($context, $name) || !$config->can($config->getID(), READ)) {
284330
return AbstractController::getAccessDeniedErrorResponse();
285331
}
286332
return new JSONResponse([

tests/functional/Glpi/Api/HL/Controller/SetupControllerTest.php

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,24 @@ public function testCRUDConfigValues()
239239
->status(static fn ($status) => $status === 204);
240240
});
241241

242+
// Can get a config value using GraphQL
243+
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==priority_2") { context, name, value } }');
244+
$this->api->call($request, function ($call) {
245+
/** @var \HLAPICallAsserter $call */
246+
$call->response
247+
->isOK()
248+
->jsonContent(function ($content) {
249+
$this->assertArrayHasKey('data', $content);
250+
$this->assertArrayHasKey('Config', $content['data']);
251+
$this->assertCount(1, $content['data']['Config']);
252+
$config = $content['data']['Config'][0];
253+
$this->assertEquals('core', $config['context']);
254+
$this->assertEquals('priority_2', $config['name']);
255+
$this->assertEquals('#ffe0e0', $config['value']);
256+
});
257+
});
258+
242259
// Cannot get an undisclosable config value using GraphQL
243-
//FIXME: There are currently no restrictions in GraphQL to avoid fetching undisclosable config values
244260
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==smtp_passwd") { context, name, value } }');
245261
$this->api->call($request, function ($call) {
246262
/** @var \HLAPICallAsserter $call */
@@ -252,6 +268,34 @@ public function testCRUDConfigValues()
252268
$this->assertEmpty($content['data']['Config']);
253269
});
254270
});
271+
272+
// Can search config values
273+
$request = new Request('GET', '/Setup/Config');
274+
$request->setParameter('filter', 'name==priority_2');
275+
$this->api->call($request, function ($call) {
276+
/** @var \HLAPICallAsserter $call */
277+
$call->response
278+
->isOK()
279+
->jsonContent(function ($content) {
280+
$this->assertCount(1, $content);
281+
$config = $content[0];
282+
$this->assertEquals('core', $config['context']);
283+
$this->assertEquals('priority_2', $config['name']);
284+
$this->assertEquals('#ffe0e0', $config['value']);
285+
});
286+
});
287+
288+
// Cannot search undisclosable config values
289+
$request = new Request('GET', '/Setup/Config');
290+
$request->setParameter('filter', 'name==smtp_passwd');
291+
$this->api->call($request, function ($call) {
292+
/** @var \HLAPICallAsserter $call */
293+
$call->response
294+
->isOK()
295+
->jsonContent(function ($content) {
296+
$this->assertEmpty($content);
297+
});
298+
});
255299
}
256300

257301
public function testConfigNotIn2_0()

0 commit comments

Comments
 (0)