Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 61 additions & 2 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1238,7 +1238,7 @@ systemStructureContext().then((site) => {
for (let systemRoute in systemRouteRegistry[systemMethod]) {
const systemRoutePath = `${systemApiV1BasePath}${systemRoute}`;
const systemRouteParser = getSystemV1RouteParser(systemMethod, systemRoute);
const systemRouteHandler = (req, res, next) => {
const systemRouteHandler = async (req, res, next) => {
const op = req.route.path.replace(systemApiV1BasePath, '');
const rMethod = req.method.toLowerCase();
if (!validateSystemV1RouteAccess(req, op)) {
Expand All @@ -1263,6 +1263,12 @@ systemStructureContext().then((site) => {
if (!enforceSystemApiUserTokenPolicy(req, res, op, rMethod, basicAuth)) {
return;
}
// F2/Q14+F3: enforce site-token policy for 'authenticated-site'
// system routes (e.g. provider-search) so the router returns a
// consistent 403 envelope instead of the handler doing the check.
if (!await enforceSystemApiSiteTokenPolicy(req, res, op, rMethod, basicAuth)) {
return;
}
return systemRouteRegistry[rMethod][op](req, res, next);
}
// D1b status-code parity (matches site API + PHP SystemApiSecurity):
Expand All @@ -1280,7 +1286,7 @@ systemStructureContext().then((site) => {
data: { message: 'Authentication required' },
});
};
const siteScopedSystemRouteHandler = (req, res, next) => {
const siteScopedSystemRouteHandler = async (req, res, next) => {
const op = req.route.path.replace(
`/${HAXCMS.sitesDirectory}/*${systemApiV1BasePath}`,
'',
Expand Down Expand Up @@ -1308,6 +1314,12 @@ systemStructureContext().then((site) => {
if (!enforceSystemApiUserTokenPolicy(req, res, op, rMethod, basicAuth)) {
return;
}
// F2/Q14+F3: enforce site-token policy for 'authenticated-site'
// system routes (e.g. provider-search) so the router returns a
// consistent 403 envelope instead of the handler doing the check.
if (!await enforceSystemApiSiteTokenPolicy(req, res, op, rMethod, basicAuth)) {
return;
}
return systemRouteRegistry[rMethod][op](req, res, next);
}
// D1b status-code parity (matches site API + PHP SystemApiSecurity):
Expand Down Expand Up @@ -1826,6 +1838,53 @@ function enforceSystemApiUserTokenPolicy(req, res, op, method, basicAuth) {
}
return true;
}
// F2/Q14+F3: site-token enforcement for 'authenticated-site' system routes
// (e.g. provider-search). Mirrors the site API's 'authenticated-site' policy
// but runs in the system route handler so the 403 envelope is consistent
// with the rest of the system API. The handler may still do semantic
// siteName validation (e.g. generateAppStore) — this function only gates on
// token presence + validity against the resolved siteName + userName.
async function enforceSystemApiSiteTokenPolicy(req, res, op, method, basicAuth) {
const policy = getSystemApiRouteAuthPolicy(op, method);
if (policy !== 'authenticated-site') {
return true;
}
const siteToken = getRequestHeaderValue(req, 'x-haxcms-site-token');
if (siteToken === '') {
res.status(403).json({
status: 403,
data: { message: 'X-HAXCMS-Site-Token header is required for this endpoint' },
});
return false;
}
const userName = resolveSystemApiAuthenticatedUserName(req, basicAuth);
if (userName === '') {
res.status(403).json({
status: 403,
data: { message: 'Unable to resolve authenticated user context' },
});
return false;
}
const siteName = await resolveSiteApiRequestSiteName(req, {
userName: userName,
siteToken: siteToken,
});
if (!siteName) {
res.status(403).json({
status: 403,
data: { message: 'Unable to resolve site token context' },
});
return false;
}
if (!HAXCMS.validateRequestToken(siteToken, `${userName}:${siteName}`)) {
res.status(403).json({
status: 403,
data: { message: 'Invalid X-HAXCMS-Site-Token header' },
});
return false;
}
return true;
}
Comment on lines +1847 to +1887
function assertSiteApiMutationRoutesAreSecured(routeRegistry = null) {
const registry =
routeRegistry && typeof routeRegistry === 'object' ? routeRegistry : {};
Expand Down
7 changes: 0 additions & 7 deletions src/lib/SystemRoutesMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -245,12 +245,6 @@ addRouteHandler(
'skeletons/:skeletonName',
settingsRoutes.getSkeleton,
);
addRouteHandler(
SystemRoutesMap,
'post',
'skeletons/:skeletonName',
settingsRoutes.getSkeleton,
);
addRouteHandler(
SystemRoutesMap,
'patch',
Expand Down Expand Up @@ -301,7 +295,6 @@ const SystemV1OpenRoutes = [
'session/connection-settings',
'session/connection-test',
'integrations/app-store',
'integrations/app-store/providers/:provider/search',
'',
'openapi',
'openapi.json',
Expand Down
11 changes: 11 additions & 0 deletions src/openapi/site-spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1214,6 +1214,7 @@ paths:
- $ref: "#/components/parameters/FilterExtension"
- $ref: "#/components/parameters/FilterStartsWith"
- $ref: "#/components/parameters/FilterNameContains"
- $ref: "#/components/parameters/FileName"
- $ref: "#/components/parameters/PageLimit"
- $ref: "#/components/parameters/PageOffset"
- $ref: "#/components/parameters/Sort"
Expand Down Expand Up @@ -2188,6 +2189,16 @@ components:
required: false
schema:
type: string
FileName:
name: filename
in: query
required: false
schema:
type: string
description: >
Substring filter applied to both the file relative path and the file
name. When supplied, only files whose path or name contains the value
(case-insensitive) are returned.
FilterKind:
name: filter.kind
in: query
Expand Down
Loading
Loading