Skip to content

Commit 918bcc2

Browse files
cconard96cedric-anne
authored andcommitted
hlapi config values
1 parent dc8112c commit 918bcc2

File tree

3 files changed

+316
-0
lines changed

3 files changed

+316
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ The present file will list all changes made to the project; according to the
66
## [11.0.2] unreleased
77

88
### Added
9+
- High-Level API endpoints for configuration settings `/Setup/Config/{context}/{name}`.
910

1011
### Changed
1112
- Added High-Level API version 2.1. Make sure you are pinning your requests to a specific version (Ex: `/api.php/v2.0`) if needed to exclude endpoints/properties added in later versions. See version pinning in the getting started documentation `/api.php/getting-started`.

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

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737

3838
use AuthLDAP;
3939
use CommonDBTM;
40+
use Config;
4041
use Glpi\Api\HL\Doc as Doc;
4142
use Glpi\Api\HL\Middleware\ResultFormatterMiddleware;
4243
use Glpi\Api\HL\ResourceAccessor;
@@ -51,6 +52,8 @@ final class SetupController extends AbstractController
5152
{
5253
public static function getRawKnownSchemas(): array
5354
{
55+
global $DB;
56+
5457
return [
5558
'LDAPDirectory' => [
5659
'x-version-introduced' => '2.0',
@@ -104,6 +107,44 @@ public static function getRawKnownSchemas(): array
104107
],
105108
],
106109
],
110+
'Config' => [
111+
'x-version-introduced' => '2.1',
112+
'x-itemtype' => Config::class,
113+
'type' => Doc\Schema::TYPE_OBJECT,
114+
'properties' => [
115+
'id' => [
116+
'type' => Doc\Schema::TYPE_INTEGER,
117+
'format' => Doc\Schema::FORMAT_INTEGER_INT64,
118+
'readOnly' => true,
119+
],
120+
'context' => ['type' => Doc\Schema::TYPE_STRING],
121+
'name' => ['type' => Doc\Schema::TYPE_STRING],
122+
'value' => ['type' => Doc\Schema::TYPE_STRING],
123+
],
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+
],
147+
],
107148
];
108149
}
109150

@@ -118,6 +159,7 @@ public static function getSetupTypes(bool $types_only = true): array
118159
if ($types === null) {
119160
$types = [
120161
'LDAPDirectory' => AuthLDAP::getTypeName(1),
162+
// Do not add Config here as it is handled specially
121163
];
122164
}
123165
return $types_only ? array_keys($types) : $types;
@@ -212,4 +254,107 @@ public function deleteItem(Request $request): Response
212254
$itemtype = $request->getAttribute('itemtype');
213255
return ResourceAccessor::deleteBySchema($this->getKnownSchema($itemtype, $this->getAPIVersion($request)), $request->getAttributes(), $request->getParameters());
214256
}
257+
258+
private static function isUndisclosedConfig(string $context, string $name): bool
259+
{
260+
$f = ['context' => $context, 'name' => $name, 'value' => 'dummy'];
261+
Config::unsetUndisclosedFields($f);
262+
return !array_key_exists('value', $f);
263+
}
264+
265+
#[Route(path: '/Config/{context}/{name}', methods: ['PATCH'], requirements: [
266+
'context' => '\w+',
267+
'name' => '\w+',
268+
], middlewares: [ResultFormatterMiddleware::class])]
269+
#[RouteVersion(introduced: '2.1')]
270+
#[Doc\UpdateRoute(schema_name: 'Config')]
271+
public function setConfigValue(Request $request): Response
272+
{
273+
// Skip using ResourceAccessor given the particularities of Config
274+
if (!Config::canUpdate()) {
275+
return AbstractController::getAccessDeniedErrorResponse();
276+
}
277+
$context = $request->getAttribute('context');
278+
$name = $request->getAttribute('name');
279+
$value = $request->getParameter('value');
280+
Config::setConfigurationValues($context, [$name => $value]);
281+
// Return the updated config
282+
if (self::isUndisclosedConfig($context, $name)) {
283+
// If the field is undisclosed, only return a 204 to indicate success without revealing the value
284+
return new JSONResponse(null, 204);
285+
}
286+
return new JSONResponse([
287+
'context' => $context,
288+
'name' => $name,
289+
'value' => Config::getConfigurationValue($context, $name),
290+
]);
291+
}
292+
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+
314+
#[Route(path: '/Config/{context}/{name}', methods: ['GET'], requirements: [
315+
'context' => '\w+',
316+
'name' => '\w+',
317+
], middlewares: [ResultFormatterMiddleware::class])]
318+
#[RouteVersion(introduced: '2.1')]
319+
#[Doc\GetRoute(schema_name: 'Config')]
320+
public function getConfigValue(Request $request): Response
321+
{
322+
// Skip using ResourceAccessor given the particularities of Config
323+
$context = $request->getAttribute('context');
324+
$name = $request->getAttribute('name');
325+
$config = new Config();
326+
if (!$config->getFromDBByCrit(['context' => $context, 'name' => $name,])) {
327+
return AbstractController::getNotFoundErrorResponse();
328+
}
329+
if (self::isUndisclosedConfig($context, $name) || !$config->can($config->getID(), READ)) {
330+
return AbstractController::getAccessDeniedErrorResponse();
331+
}
332+
return new JSONResponse([
333+
'context' => $context,
334+
'name' => $name,
335+
'value' => Config::getConfigurationValue($context, $name),
336+
]);
337+
}
338+
339+
#[Route(path: '/Config/{context}/{name}', methods: ['DELETE'], requirements: [
340+
'context' => '\w+',
341+
'name' => '\w+',
342+
])]
343+
#[RouteVersion(introduced: '2.1')]
344+
#[Doc\DeleteRoute(schema_name: 'Config')]
345+
public function deleteConfigValue(Request $request): Response
346+
{
347+
// Skip using ResourceAccessor given the particularities of Config
348+
if (!Config::canUpdate()) {
349+
return AbstractController::getAccessDeniedErrorResponse();
350+
}
351+
$context = $request->getAttribute('context');
352+
$name = $request->getAttribute('name');
353+
$config = new Config();
354+
if (!$config->getFromDBByCrit(['context' => $context, 'name' => $name])) {
355+
return AbstractController::getNotFoundErrorResponse();
356+
}
357+
Config::deleteConfigurationValues($context, [$name]);
358+
return new JSONResponse(null, 204);
359+
}
215360
}

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

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
namespace tests\units\Glpi\Api\HL\Controller;
3636

3737
use AuthLDAP;
38+
use Config;
3839
use Glpi\Api\HL\Middleware\InternalAuthMiddleware;
3940
use Glpi\Http\Request;
4041

@@ -156,4 +157,173 @@ public function testCRUDNoRights()
156157
});
157158
});
158159
}
160+
161+
public function testCRUDConfigValues()
162+
{
163+
$this->loginWeb();
164+
165+
$this->api->getRouter()->registerAuthMiddleware(new InternalAuthMiddleware());
166+
// Can get a config value
167+
$this->api->call(new Request('GET', '/Setup/Config/core/priority_1'), function ($call) {
168+
/** @var \HLAPICallAsserter $call */
169+
$call->response
170+
->isOK()
171+
->jsonContent(function ($content) {
172+
$this->assertEquals('priority_1', $content['name']);
173+
$this->assertEquals('core', $content['context']);
174+
$this->assertEquals('#fff2f2', $content['value']);
175+
});
176+
});
177+
178+
// Get an undisclosable config value
179+
Config::setConfigurationValues('core', ['smtp_passwd' => 'test']);
180+
$this->api->call(new Request('GET', '/Setup/Config/core/smtp_passwd'), function ($call) {
181+
/** @var \HLAPICallAsserter $call */
182+
$call->response->isAccessDenied();
183+
});
184+
185+
// Not existing config value
186+
$this->api->call(new Request('GET', '/Setup/Config/core/notrealconfig'), function ($call) {
187+
/** @var \HLAPICallAsserter $call */
188+
$call->response->isNotFoundError();
189+
});
190+
191+
// Can update a config value
192+
$request = new Request('PATCH', '/Setup/Config/core/priority_1');
193+
$request->setParameter('value', '#ffffff');
194+
$this->api->call($request, function ($call) {
195+
/** @var \HLAPICallAsserter $call */
196+
$call->response
197+
->isOK()
198+
->jsonContent(function ($content) {
199+
$this->assertEquals('priority_1', $content['name']);
200+
$this->assertEquals('core', $content['context']);
201+
$this->assertEquals('#ffffff', $content['value']);
202+
});
203+
});
204+
$this->api->call(new Request('GET', '/Setup/Config/core/priority_1'), function ($call) {
205+
/** @var \HLAPICallAsserter $call */
206+
$call->response
207+
->isOK()
208+
->jsonContent(function ($content) {
209+
$this->assertEquals('priority_1', $content['name']);
210+
$this->assertEquals('core', $content['context']);
211+
$this->assertEquals('#ffffff', $content['value']);
212+
});
213+
});
214+
215+
// Can update an undisclosable config value
216+
$request = new Request('PATCH', '/Setup/Config/core/smtp_passwd');
217+
$request->setParameter('value', 'newtest');
218+
$this->api->call($request, function ($call) {
219+
/** @var \HLAPICallAsserter $call */
220+
$call->response
221+
->status(static fn($status) => $status === 204);
222+
});
223+
224+
// Can delete a config value
225+
$this->api->call(new Request('DELETE', '/Setup/Config/core/priority_1'), function ($call) {
226+
/** @var \HLAPICallAsserter $call */
227+
$call->response
228+
->status(static fn($status) => $status === 204);
229+
});
230+
$this->api->call(new Request('GET', '/Setup/Config/core/priority_1'), function ($call) {
231+
/** @var \HLAPICallAsserter $call */
232+
$call->response->isNotFoundError();
233+
});
234+
235+
// Can delete an undisclosable config value
236+
$this->api->call(new Request('DELETE', '/Setup/Config/core/smtp_passwd'), function ($call) {
237+
/** @var \HLAPICallAsserter $call */
238+
$call->response
239+
->status(static fn($status) => $status === 204);
240+
});
241+
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+
259+
// Cannot get an undisclosable config value using GraphQL
260+
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==smtp_passwd") { context, name, value } }');
261+
$this->api->call($request, function ($call) {
262+
/** @var \HLAPICallAsserter $call */
263+
$call->response
264+
->isOK()
265+
->jsonContent(function ($content) {
266+
$this->assertArrayHasKey('data', $content);
267+
$this->assertArrayHasKey('Config', $content['data']);
268+
$this->assertEmpty($content['data']['Config']);
269+
});
270+
});
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+
});
299+
}
300+
301+
public function testConfigNotIn2_0()
302+
{
303+
$this->login();
304+
305+
$v2_api = $this->api->withVersion('2.0.0');
306+
$v2_api->call(new Request('GET', '/Setup/Config/core/test'), function ($call) {
307+
/** @var \HLAPICallAsserter $call */
308+
$call->response->isNotFoundError();
309+
});
310+
$v2_api->call(new Request('PATCH', '/Setup/Config/core/test'), function ($call) {
311+
/** @var \HLAPICallAsserter $call */
312+
$call->response->isNotFoundError();
313+
});
314+
$v2_api->call(new Request('DELETE', '/Setup/Config/core/test'), function ($call) {
315+
/** @var \HLAPICallAsserter $call */
316+
$call->response->isNotFoundError();
317+
});
318+
319+
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==test") { context, name, value } }');
320+
$v2_api->call($request, function ($call) {
321+
/** @var \HLAPICallAsserter $call */
322+
$call->response
323+
->isOK()
324+
->jsonContent(function ($content) {
325+
$this->assertArrayHasKey('errors', $content);
326+
});
327+
});
328+
}
159329
}

0 commit comments

Comments
 (0)