Skip to content

Commit 45a8345

Browse files
committed
hlapi config values
1 parent 3ca9703 commit 45a8345

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,11 @@ The present file will list all changes made to the project; according to the
2929
## [11.0.1] 2025-10-09
3030

3131
### Added
32+
- High-Level API endpoints for configuration settings `/Setup/Config/{context}/{name}`.
3233

3334
### Changed
35+
- 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`.
36+
- High-Level API responses for not found routes now correctly return a body including the standard error properties (status, title, detail). This is not controlled by the API version.
3437

3538
### Deprecated
3639

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

Lines changed: 99 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;
@@ -104,6 +105,21 @@ public static function getRawKnownSchemas(): array
104105
],
105106
],
106107
],
108+
'Config' => [
109+
'x-version-introduced' => '2.1',
110+
'x-itemtype' => Config::class,
111+
'type' => Doc\Schema::TYPE_OBJECT,
112+
'properties' => [
113+
'id' => [
114+
'type' => Doc\Schema::TYPE_INTEGER,
115+
'format' => Doc\Schema::FORMAT_INTEGER_INT64,
116+
'readOnly' => true,
117+
],
118+
'context' => ['type' => Doc\Schema::TYPE_STRING],
119+
'name' => ['type' => Doc\Schema::TYPE_STRING],
120+
'value' => ['type' => Doc\Schema::TYPE_STRING],
121+
],
122+
],
107123
];
108124
}
109125

@@ -118,6 +134,7 @@ public static function getSetupTypes(bool $types_only = true): array
118134
if ($types === null) {
119135
$types = [
120136
'LDAPDirectory' => AuthLDAP::getTypeName(1),
137+
// Do not add Config here as it is handled specially
121138
];
122139
}
123140
return $types_only ? array_keys($types) : $types;
@@ -212,4 +229,86 @@ public function deleteItem(Request $request): Response
212229
$itemtype = $request->getAttribute('itemtype');
213230
return ResourceAccessor::deleteBySchema($this->getKnownSchema($itemtype, $this->getAPIVersion($request)), $request->getAttributes(), $request->getParameters());
214231
}
232+
233+
private function isUndisclosedConfig(string $context, string $name): bool
234+
{
235+
$f = ['context' => $context, 'name' => $name, 'value' => 'dummy'];
236+
Config::unsetUndisclosedFields($f);
237+
return !array_key_exists('value', $f);
238+
}
239+
240+
#[Route(path: '/Config/{context}/{name}', methods: ['PATCH'], requirements: [
241+
'context' => '\w+',
242+
'name' => '\w+',
243+
], middlewares: [ResultFormatterMiddleware::class])]
244+
#[RouteVersion(introduced: '2.1')]
245+
#[Doc\UpdateRoute(schema_name: 'Config')]
246+
public function setConfigValue(Request $request): Response
247+
{
248+
// Skip using ResourceAccessor given the particularities of Config
249+
if (!Config::canUpdate()) {
250+
return AbstractController::getAccessDeniedErrorResponse();
251+
}
252+
$context = $request->getAttribute('context');
253+
$name = $request->getAttribute('name');
254+
$value = $request->getParameter('value');
255+
Config::setConfigurationValues($context, [$name => $value]);
256+
// Return the updated config
257+
if ($this->isUndisclosedConfig($context, $name)) {
258+
// If the field is undisclosed, only return a 204 to indicate success without revealing the value
259+
return new JSONResponse(null, 204);
260+
}
261+
return new JSONResponse([
262+
'context' => $context,
263+
'name' => $name,
264+
'value' => Config::getConfigurationValue($context, $name),
265+
]);
266+
}
267+
268+
#[Route(path: '/Config/{context}/{name}', methods: ['GET'], requirements: [
269+
'context' => '\w+',
270+
'name' => '\w+',
271+
], middlewares: [ResultFormatterMiddleware::class])]
272+
#[RouteVersion(introduced: '2.1')]
273+
#[Doc\GetRoute(schema_name: 'Config')]
274+
public function getConfigValue(Request $request): Response
275+
{
276+
// Skip using ResourceAccessor given the particularities of Config
277+
$context = $request->getAttribute('context');
278+
$name = $request->getAttribute('name');
279+
$config = new Config();
280+
if (!$config->getFromDBByCrit(['context' => $context, 'name' => $name,])) {
281+
return AbstractController::getNotFoundErrorResponse();
282+
}
283+
if ($this->isUndisclosedConfig($context, $name) || !$config->can($config->getID(), READ)) {
284+
return AbstractController::getAccessDeniedErrorResponse();
285+
}
286+
return new JSONResponse([
287+
'context' => $context,
288+
'name' => $name,
289+
'value' => Config::getConfigurationValue($context, $name),
290+
]);
291+
}
292+
293+
#[Route(path: '/Config/{context}/{name}', methods: ['DELETE'], requirements: [
294+
'context' => '\w+',
295+
'name' => '\w+',
296+
])]
297+
#[RouteVersion(introduced: '2.1')]
298+
#[Doc\DeleteRoute(schema_name: 'Config')]
299+
public function deleteConfigValue(Request $request): Response
300+
{
301+
// Skip using ResourceAccessor given the particularities of Config
302+
if (!Config::canUpdate()) {
303+
return AbstractController::getAccessDeniedErrorResponse();
304+
}
305+
$context = $request->getAttribute('context');
306+
$name = $request->getAttribute('name');
307+
$config = new Config();
308+
if (!$config->getFromDBByCrit(['context' => $context, 'name' => $name])) {
309+
return AbstractController::getNotFoundErrorResponse();
310+
}
311+
Config::deleteConfigurationValues($context, [$name]);
312+
return new JSONResponse(null, 204);
313+
}
215314
}

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

Lines changed: 126 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,129 @@ 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+
// Cannot get an undisclosable config value using GraphQL
243+
//FIXME: There are currently no restrictions in GraphQL to avoid fetching undisclosable config values
244+
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==smtp_passwd") { context, name, value } }');
245+
$this->api->call($request, function ($call) {
246+
/** @var \HLAPICallAsserter $call */
247+
$call->response
248+
->isOK()
249+
->jsonContent(function ($content) {
250+
$this->assertArrayHasKey('data', $content);
251+
$this->assertArrayHasKey('Config', $content['data']);
252+
$this->assertEmpty($content['data']['Config']);
253+
});
254+
});
255+
}
256+
257+
public function testConfigNotIn2_0()
258+
{
259+
$this->login();
260+
261+
$v2_api = $this->api->withVersion('2.0.0');
262+
$v2_api->call(new Request('GET', '/Setup/Config/core/test'), function ($call) {
263+
/** @var \HLAPICallAsserter $call */
264+
$call->response->isNotFoundError();
265+
});
266+
$v2_api->call(new Request('PATCH', '/Setup/Config/core/test'), function ($call) {
267+
/** @var \HLAPICallAsserter $call */
268+
$call->response->isNotFoundError();
269+
});
270+
$v2_api->call(new Request('DELETE', '/Setup/Config/core/test'), function ($call) {
271+
/** @var \HLAPICallAsserter $call */
272+
$call->response->isNotFoundError();
273+
});
274+
275+
$request = new Request('POST', '/GraphQL', [], 'query { Config(filter: "context==core;name==test") { context, name, value } }');
276+
$v2_api->call($request, function ($call) {
277+
/** @var \HLAPICallAsserter $call */
278+
$call->response
279+
->isOK()
280+
->jsonContent(function ($content) {
281+
$this->assertArrayHasKey('errors', $content);
282+
});
283+
});
284+
}
159285
}

0 commit comments

Comments
 (0)