Skip to content

Commit 5ffd826

Browse files
committed
https://github.com/haxtheweb/issues/issues/2867
1 parent c9cc419 commit 5ffd826

5 files changed

Lines changed: 125 additions & 7 deletions

File tree

system/backend/php/lib/systemRoutes/SystemApiSecurity.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ public static function validateSystemApiAccess($context, $route, $method)
7676
}
7777
private static function getRouteSecurity($route, $method)
7878
{
79+
$normalizedMethod = strtoupper((string) $method);
7980
$publicRoutes = array(
8081
'v1',
8182
'v1/openapi',
@@ -92,6 +93,23 @@ private static function getRouteSecurity($route, $method)
9293
if (in_array($route, $publicRoutes, true)) {
9394
return 'public';
9495
}
96+
$authenticatedRoutes = array(
97+
'v1/haxiamAddUserAccess',
98+
);
99+
if (in_array($route, $authenticatedRoutes, true)) {
100+
return 'authenticated';
101+
}
102+
$dashboardReadRoutes = array(
103+
'v1/skeletons',
104+
'v1/skeletons/:skeletonName',
105+
'v1/themes',
106+
);
107+
if (
108+
$normalizedMethod === 'GET' &&
109+
in_array($route, $dashboardReadRoutes, true)
110+
) {
111+
return 'authenticated';
112+
}
95113
$adminRoutes = array(
96114
'v1/configuration/api-keys',
97115
'v1/configuration/media',
@@ -100,7 +118,6 @@ private static function getRouteSecurity($route, $method)
100118
'v1/skeletons',
101119
'v1/skeletons/:skeletonName',
102120
'v1/themes',
103-
'v1/haxiamAddUserAccess',
104121
);
105122
if (in_array($route, $adminRoutes, true)) {
106123
return 'admin';

system/backend/php/lib/systemRoutes/v1/lifecycle.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
unset($operations->rawParams['jwt']);
1818
unset($operations->rawParams['user_token']);
1919
unset($operations->rawParams['site_token']);
20-
$activeUser = $GLOBALS['HAXCMS']->getActiveUserName();
21-
$userToken = $GLOBALS['HAXCMS']->getRequestToken($activeUser);
20+
$tokenUser = $GLOBALS['HAXCMS']->getRequestTokenUserName();
21+
if (!is_string($tokenUser) || $tokenUser === '') {
22+
$tokenUser = $GLOBALS['HAXCMS']->getActiveUserName();
23+
}
24+
$userToken = $GLOBALS['HAXCMS']->getRequestToken($tokenUser);
2225
$operations->params['user_token'] = $userToken;
2326
$operations->rawParams['user_token'] = $userToken;
2427
if (isset($context->params['siteName'])) {

system/backend/php/lib/systemRoutes/v1/settings.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@ function haxcmsSystemSettingsInvokeAsPost($operationCallback)
4848
unset($operations->rawParams['jwt']);
4949
unset($operations->rawParams['user_token']);
5050
unset($operations->rawParams['site_token']);
51-
$activeUser = $GLOBALS['HAXCMS']->getActiveUserName();
52-
$userToken = $GLOBALS['HAXCMS']->getRequestToken($activeUser);
51+
$tokenUser = $GLOBALS['HAXCMS']->getRequestTokenUserName();
52+
if (!is_string($tokenUser) || $tokenUser === '') {
53+
$tokenUser = $GLOBALS['HAXCMS']->getActiveUserName();
54+
}
55+
$userToken = $GLOBALS['HAXCMS']->getRequestToken($tokenUser);
5356
$operations->params['user_token'] = $userToken;
57+
$operations->rawParams['user_token'] = $userToken;
5458
$route = $context->routeSuffix;
5559
$method = $context->method;
5660
$response = null;

system/backend/php/tests/systemRoutes/SystemRoutesTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,9 +196,15 @@ function assertEquals($expected, $actual, $message = '') {
196196
$jwtStringOther = JWT::encode($jwtPayloadOther, 'test-private-key' . 'test-salt');
197197
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $jwtStringOther;
198198
$authCtxOther = SystemApiRequestContext::create();
199+
$themesReadResult = SystemApiSecurity::validateSystemApiAccess($authCtxOther, 'v1/themes', 'GET');
200+
assertTrue($themesReadResult['allowed'], "Themes GET allowed for authenticated dashboard user");
201+
assertEquals(200, $themesReadResult['status'], "Themes GET returns 200 for authenticated dashboard user");
199202
$adminResult2 = SystemApiSecurity::validateSystemApiAccess($authCtxOther, 'v1/themes', 'POST');
200203
assertTrue(!$adminResult2['allowed'], "Admin route denied for non-admin user");
201204
assertEquals(403, $adminResult2['status'], "Admin route returns 403 for non-admin");
205+
$shareAccessResult = SystemApiSecurity::validateSystemApiAccess($authCtxOther, 'v1/haxiamAddUserAccess', 'POST');
206+
assertTrue($shareAccessResult['allowed'], "haxiamAddUserAccess allowed for authenticated dashboard user");
207+
assertEquals(200, $shareAccessResult['status'], "haxiamAddUserAccess returns 200 for authenticated dashboard user");
202208

203209
// Admin route with super user should be allowed
204210
$jwtPayloadAdmin = array('id' => 'test-id', 'iat' => time(), 'exp' => time() + 900, 'user' => 'admin');

system/backend/php/tests/v1-integration-tests.php

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,14 @@ function assertJsonValid($json, $message) {
7272
assertTrue(strpos($settings->logout, '?site_token=') === false, 'logout v1 path has no query token');
7373
assertTrue(strpos($settings->systemApiBasePath, '?user_token=') === false, 'systemApiBasePath has no query token');
7474
$existingReferer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : null;
75-
$existingRequestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
75+
$existingRequestUri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : null;
76+
$existingAuthHeader = isset($_SERVER['HTTP_AUTHORIZATION']) ? $_SERVER['HTTP_AUTHORIZATION'] : null;
77+
$existingRefreshCookie = isset($_COOKIE['haxcms_refresh_token']) ? $_COOKIE['haxcms_refresh_token'] : null;
78+
$existingSessionJwt = isset($HAXCMS->sessionJwt) ? $HAXCMS->sessionJwt : null;
79+
$existingIamSetting = isset($HAXCMS->config->iam) ? $HAXCMS->config->iam : null;
80+
$existingUserName = isset($HAXCMS->user->name) ? $HAXCMS->user->name : null;
81+
$existingUserPassword = isset($HAXCMS->user->password) ? $HAXCMS->user->password : null;
82+
$existingServerSoftware = isset($_SERVER['SERVER_SOFTWARE']) ? $_SERVER['SERVER_SOFTWARE'] : null;
7683
$_SERVER['REQUEST_URI'] = '/';
7784
$_SERVER['HTTP_REFERER'] = 'https://example.com/';
7885
$dashboardRefererSettings = $HAXCMS->appJWTConnectionSettings();
@@ -108,6 +115,51 @@ function assertJsonValid($json, $message) {
108115
assertTrue(strpos($siteRefererSettings->logout, '/') === 0, 'site-context logout path is root-absolute at root install');
109116
assertTrue(strpos($siteRefererSettings->systemApiBasePath, '/') === 0, 'site-context systemApiBasePath is root-absolute at root install');
110117
}
118+
$HAXCMS->config->iam = true;
119+
$HAXCMS->user->name = null;
120+
$HAXCMS->user->password = null;
121+
$_SERVER['SERVER_SOFTWARE'] = 'TestServer';
122+
$HAXCMS->addEventListener('haxcms-validate-user', function (&$usr) {
123+
if (isset($usr->name) && $usr->name === 'tenant-user') {
124+
$usr->grantAccess = true;
125+
}
126+
});
127+
$tenantJwt = $HAXCMS->getJWT('tenant-user');
128+
$_SERVER['HTTP_AUTHORIZATION'] = 'Bearer ' . $tenantJwt;
129+
$_COOKIE['haxcms_refresh_token'] = $HAXCMS->getRefreshToken('tenant-user');
130+
$HAXCMS->sessionJwt = $tenantJwt;
131+
$_SERVER['REQUEST_URI'] = '/tenant-user/system/api/v1/sites';
132+
$_SERVER['HTTP_REFERER'] = 'https://example.com/tenant-user/';
133+
$iamActiveUser = $HAXCMS->getActiveUserName();
134+
$legacyIamUserToken = $HAXCMS->getRequestToken($iamActiveUser);
135+
assertTrue(
136+
!$HAXCMS->validateRequestToken($legacyIamUserToken, $iamActiveUser),
137+
'IAM token mismatch: active-user token is invalid when authenticated tenant differs'
138+
);
139+
$requestTokenUser = $HAXCMS->getRequestTokenUserName();
140+
$requestScopedToken = $HAXCMS->getRequestToken($requestTokenUser);
141+
assertTrue(
142+
$HAXCMS->validateRequestToken($requestScopedToken, $iamActiveUser),
143+
'IAM token match: request-token user token validates for legacy active-user value'
144+
);
145+
$lifecycleHandler = include $repoRoot . '/system/backend/php/lib/systemRoutes/v1/lifecycle.php';
146+
$lifecycleContext = new stdClass();
147+
$lifecycleContext->apiBasePath = '/tenant-user/system/api';
148+
$lifecycleContext->body = array();
149+
$lifecycleContext->params = array();
150+
$lifecycleContext->routeSuffix = 'v1/sites';
151+
$lifecycleContext->method = 'GET';
152+
ob_start();
153+
$lifecycleHandler($lifecycleContext);
154+
$iamSitesResponseRaw = ob_get_clean();
155+
$iamSitesResponse = json_decode($iamSitesResponseRaw, true);
156+
assertTrue(is_array($iamSitesResponse), 'IAM lifecycle listSites returns JSON');
157+
assertEquals(
158+
200,
159+
isset($iamSitesResponse['status']) ? $iamSitesResponse['status'] : null,
160+
'IAM lifecycle listSites returns 200 with request-scoped token user'
161+
);
162+
$HAXCMS->addEventListener('haxcms-validate-user', false);
111163
if ($existingReferer !== null) {
112164
$_SERVER['HTTP_REFERER'] = $existingReferer;
113165
}
@@ -120,6 +172,38 @@ function assertJsonValid($json, $message) {
120172
else {
121173
unset($_SERVER['REQUEST_URI']);
122174
}
175+
if ($existingAuthHeader !== null) {
176+
$_SERVER['HTTP_AUTHORIZATION'] = $existingAuthHeader;
177+
}
178+
else {
179+
unset($_SERVER['HTTP_AUTHORIZATION']);
180+
}
181+
if ($existingRefreshCookie !== null) {
182+
$_COOKIE['haxcms_refresh_token'] = $existingRefreshCookie;
183+
}
184+
else {
185+
unset($_COOKIE['haxcms_refresh_token']);
186+
}
187+
if ($existingSessionJwt !== null) {
188+
$HAXCMS->sessionJwt = $existingSessionJwt;
189+
}
190+
else {
191+
$HAXCMS->sessionJwt = null;
192+
}
193+
if ($existingIamSetting !== null) {
194+
$HAXCMS->config->iam = $existingIamSetting;
195+
}
196+
else {
197+
unset($HAXCMS->config->iam);
198+
}
199+
$HAXCMS->user->name = $existingUserName;
200+
$HAXCMS->user->password = $existingUserPassword;
201+
if ($existingServerSoftware !== null) {
202+
$_SERVER['SERVER_SOFTWARE'] = $existingServerSoftware;
203+
}
204+
else {
205+
unset($_SERVER['SERVER_SOFTWARE']);
206+
}
123207

124208
// Test 3: SystemApiRouter file existence and route map
125209
echo "[3/13] SystemApiRouter route map...\n";
@@ -161,7 +245,11 @@ function assertJsonValid($json, $message) {
161245
assertEquals('admin', $getRouteSecurity->invoke(null, 'v1/configuration/api-keys', 'GET'), 'v1/configuration/api-keys is admin');
162246
assertEquals('admin', $getRouteSecurity->invoke(null, 'v1/blocks', 'GET'), 'v1/blocks is admin');
163247
assertEquals('authenticated', $getRouteSecurity->invoke(null, 'v1/sites', 'GET'), 'v1/sites is authenticated');
164-
assertEquals('admin', $getRouteSecurity->invoke(null, 'v1/haxiamAddUserAccess', 'POST'), 'haxiamAddUserAccess is admin');
248+
assertEquals('authenticated', $getRouteSecurity->invoke(null, 'v1/themes', 'GET'), 'v1/themes GET is authenticated');
249+
assertEquals('admin', $getRouteSecurity->invoke(null, 'v1/themes', 'POST'), 'v1/themes POST is admin');
250+
assertEquals('authenticated', $getRouteSecurity->invoke(null, 'v1/skeletons', 'GET'), 'v1/skeletons GET is authenticated');
251+
assertEquals('admin', $getRouteSecurity->invoke(null, 'v1/skeletons', 'POST'), 'v1/skeletons POST is admin');
252+
assertEquals('authenticated', $getRouteSecurity->invoke(null, 'v1/haxiamAddUserAccess', 'POST'), 'haxiamAddUserAccess is authenticated');
165253

166254
// Test 6: SystemApiSecurity bearer token validation stub (no real token)
167255
echo "[6/13] SystemApiSecurity bearer validation (no token)...\n";

0 commit comments

Comments
 (0)