From 401957ff6ce04e202ab3afe768f024c097f31c78 Mon Sep 17 00:00:00 2001 From: Abhishek Chatterjee Date: Sat, 9 May 2026 17:12:48 +0530 Subject: [PATCH] feat(ssp): ROSS-161: integrate JWT authentication into preValidation to enable user-based Server-Side Policies (SSP) --- example_config.json | 2 +- src/routes/aggregate/aggregate.ts | 9 ++++- src/routes/custom-queries/custom-queries.ts | 6 ++- src/routes/operations/delete.ts | 7 ++-- src/routes/operations/edit.ts | 14 ++++--- src/routes/operations/get-all.ts | 8 ++-- src/routes/operations/index-route.ts | 6 ++- src/routes/operations/post.ts | 6 ++- src/routes/operations/search.ts | 6 ++- src/types/fastify.d.ts | 18 ++++++--- src/utils/ssp.ts | 10 ++++- tests/utils/ssp.test.ts | 45 +++++++++++++++++++++ 12 files changed, 108 insertions(+), 29 deletions(-) diff --git a/example_config.json b/example_config.json index a91a49c..ea0b29d 100644 --- a/example_config.json +++ b/example_config.json @@ -193,7 +193,7 @@ { "paramType": "query", "paramName": "operations", - "value": "min,max" + "value": "[userEmail]" } ] } diff --git a/src/routes/aggregate/aggregate.ts b/src/routes/aggregate/aggregate.ts index 3e706e3..5457d7c 100644 --- a/src/routes/aggregate/aggregate.ts +++ b/src/routes/aggregate/aggregate.ts @@ -66,8 +66,8 @@ export function registerAggregateRoutes( `/${model.name}/aggregation/${field.name}`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { + // doing validation here because we need the user for SSP if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -83,6 +83,9 @@ export function registerAggregateRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { @@ -103,6 +106,8 @@ export function registerAggregateRoutes( .map(s => s.trim()) .filter(Boolean); + console.log({requestedOps}); + // validation: we must have at least one valid operation if (requestedOps.length === 0) { return reply diff --git a/src/routes/custom-queries/custom-queries.ts b/src/routes/custom-queries/custom-queries.ts index e942130..a3b652a 100644 --- a/src/routes/custom-queries/custom-queries.ts +++ b/src/routes/custom-queries/custom-queries.ts @@ -231,8 +231,7 @@ export function registerCustomQueryRoutes( method: cq.method, url: routePath, schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -248,6 +247,9 @@ export function registerCustomQueryRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/delete.ts b/src/routes/operations/delete.ts index ea77ef9..b14c376 100644 --- a/src/routes/operations/delete.ts +++ b/src/routes/operations/delete.ts @@ -63,9 +63,7 @@ export function registerDeleteRoutes( `/${model.name}/${field.name}/:${field.name}`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { - // checking the authorization + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -81,6 +79,9 @@ export function registerDeleteRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/edit.ts b/src/routes/operations/edit.ts index a5d01ba..06bf66c 100644 --- a/src/routes/operations/edit.ts +++ b/src/routes/operations/edit.ts @@ -230,8 +230,7 @@ export function registerEditRoutes( `/${model.name}/${field.name}/:${field.name}`, { schema: buildRouteSchema('PATCH'), - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -247,6 +246,9 @@ export function registerEditRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { @@ -266,9 +268,8 @@ export function registerEditRoutes( `/${model.name}/${field.name}/:${field.name}`, { schema: buildRouteSchema('PUT'), - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { - if (config.auth?.enableAuth) { + preValidation: async (request, reply) => { + if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); } catch { @@ -283,6 +284,9 @@ export function registerEditRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/get-all.ts b/src/routes/operations/get-all.ts index 8f221d3..ad509a3 100644 --- a/src/routes/operations/get-all.ts +++ b/src/routes/operations/get-all.ts @@ -59,12 +59,9 @@ export function registerGetAllRoutes( `/${model.name}/`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { - console.log('I am here'); + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { - console.log('I am running to verify JWT?'); await request.jwtVerify(); } catch { return reply @@ -78,6 +75,9 @@ export function registerGetAllRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/index-route.ts b/src/routes/operations/index-route.ts index b4c8a5f..496bdc4 100644 --- a/src/routes/operations/index-route.ts +++ b/src/routes/operations/index-route.ts @@ -71,8 +71,7 @@ export function registerIndexRoutes( `/${model.name}/${field.name}/:${field.name}`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -88,6 +87,9 @@ export function registerIndexRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/post.ts b/src/routes/operations/post.ts index ac60066..80c976e 100644 --- a/src/routes/operations/post.ts +++ b/src/routes/operations/post.ts @@ -54,8 +54,7 @@ export function registerPostRoutes( `/${model.name}/`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -71,6 +70,9 @@ export function registerPostRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/routes/operations/search.ts b/src/routes/operations/search.ts index 5d14130..66bf377 100644 --- a/src/routes/operations/search.ts +++ b/src/routes/operations/search.ts @@ -66,8 +66,7 @@ export function registerSearchRoutes( `/${model.name}/search/${field.name}`, { schema, - preValidation: async request => enforceSSP(sspConfig, request), - preHandler: async (request, reply) => { + preValidation: async (request, reply) => { if (config.auth?.enableAuth && authorization) { try { await request.jwtVerify(); @@ -83,6 +82,9 @@ export function registerSearchRoutes( ); } } + enforceSSP(sspConfig, request); + }, + preHandler: async request => { await callWebhook('request', webhookConfig, request, null, app.log); }, onSend: async (request, _, payload) => { diff --git a/src/types/fastify.d.ts b/src/types/fastify.d.ts index c496a23..7690343 100644 --- a/src/types/fastify.d.ts +++ b/src/types/fastify.d.ts @@ -1,11 +1,19 @@ -import {FastifyJWT} from '@fastify/jwt'; - -import {DatabaseQuery, StructuredResponse} from '@/interfaces'; - +import '@fastify/jwt'; import 'fastify'; import type Redis from 'ioredis'; +import {DatabaseQuery, StructuredResponse} from '@/interfaces'; + +declare module '@fastify/jwt' { + interface FastifyJWT { + user: { + id: string; + email: string; + }; + } +} + declare module 'fastify' { interface FastifyInstance { db: DatabaseQuery; @@ -16,6 +24,6 @@ declare module 'fastify' { raw_data?: R, ) => StructuredResponse; redis?: Redis; - jwt: FastifyJWT; + jwt: import('@fastify/jwt').JWT; } } diff --git a/src/utils/ssp.ts b/src/utils/ssp.ts index f2ab0a3..1a488b2 100644 --- a/src/utils/ssp.ts +++ b/src/utils/ssp.ts @@ -11,7 +11,15 @@ export function enforceSSP(ssps: SspConfig[], request: FastifyRequest) { const record = val as Record; ssps.forEach(ssp => { if (ssp.paramType === type) { - record[ssp.paramName] = ssp.value; + // if ssp.value is a magic variable then replace it with request.user + // aloowed magic variables are [userId] and [userEmail] + if (ssp.value === '[userId]') { + record[ssp.paramName] = request.user?.id; + } else if (ssp.value === '[userEmail]') { + record[ssp.paramName] = request.user?.email; + } else { + record[ssp.paramName] = ssp.value; + } } }); } diff --git a/tests/utils/ssp.test.ts b/tests/utils/ssp.test.ts index aded4de..b72406e 100644 --- a/tests/utils/ssp.test.ts +++ b/tests/utils/ssp.test.ts @@ -134,4 +134,49 @@ describe('enforceSSP', () => { expect(request.query).toEqual({duplicateKey: 'secondValue'}); }); + + it('should replace [userId] magic variable with request.user.id', () => { + const request = { + query: {}, + user: {id: 42, email: 'test@example.com'}, + } as unknown as FastifyRequest; + + const ssps: SspConfig[] = [ + {paramType: 'query', paramName: 'ownerId', value: '[userId]'}, + ]; + + enforceSSP(ssps, request); + + expect(request.query).toEqual({ownerId: 42}); + }); + + it('should replace [userEmail] magic variable with request.user.email', () => { + const request = { + body: {}, + user: {id: 42, email: 'test@example.com'}, + } as unknown as FastifyRequest; + + const ssps: SspConfig[] = [ + {paramType: 'body', paramName: 'user_email', value: '[userEmail]'}, + ]; + + enforceSSP(ssps, request); + + expect(request.body).toEqual({user_email: 'test@example.com'}); + }); + + it('should handle missing request.user when magic variables are used', () => { + const request = { + query: {}, + // no user object + } as unknown as FastifyRequest; + + const ssps: SspConfig[] = [ + {paramType: 'query', paramName: 'ownerId', value: '[userId]'}, + ]; + + enforceSSP(ssps, request); + + expect(request.query).toEqual({ownerId: undefined}); + }); });