Skip to content

Commit

Permalink
feat(server/trpc): start rbac refactor
Browse files Browse the repository at this point in the history
Started off by refactoring the getPageData request. Also refactoring the file structure for easier maintainence/contribution
  • Loading branch information
arthur-rl committed Jan 9, 2024
1 parent 5dc8f32 commit cbf2603
Show file tree
Hide file tree
Showing 5 changed files with 90 additions and 43 deletions.
11 changes: 9 additions & 2 deletions apps/app/server/trpc/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { TRPCError, type inferAsyncReturnType } from '@trpc/server';
import { type CreateNextContextOptions } from '@trpc/server/adapters/next';
import { prisma } from '@server/db/client';
import { IncomingHttpHeaders } from 'http';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import { JWTPayload, createRemoteJWKSet, jwtVerify } from 'jose';
import { retrieveRawUserInfoServer } from 'identity';
import axios from 'axios';
import { V2Session } from 'identity';
Expand Down Expand Up @@ -31,6 +31,10 @@ type CreateContextOptions = {
headers?: IncomingHttpHeaders;
};

interface Payload extends JWTPayload {
scope: string;
};

/** Use this helper for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
Expand All @@ -48,6 +52,7 @@ export const createContextInner = async (opts: CreateContextOptions) => {
* This is the actual context you'll use in your router
* @link https://trpc.io/docs/context
**/

export const createContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;

Expand All @@ -59,14 +64,15 @@ export const createContext = async (opts: CreateNextContextOptions) => {
headers,
});
}
const { payload } = await jwtVerify(
const { payload } = await jwtVerify<Payload>(
token,
createRemoteJWKSet(new URL(process.env.NEXT_PUBLIC_JWKS_ENDPOINT)),
{
issuer: process.env.NEXT_PUBLIC_ID_ISSUER,
audience: process.env.NEXT_PUBLIC_RESOURCE_AUDIENCE,
},
);
console.log("payload", payload)

This comment has been minimized.

Copy link
@ceriddenn

ceriddenn Mar 2, 2024

Contributor

Remove console.log (debugging)

// Create the server session object from varius data endpoints.
// grabs the logto user data.
const user_data = await retrieveRawUserInfoServer(req.cookies);
Expand Down Expand Up @@ -98,6 +104,7 @@ export const createContext = async (opts: CreateNextContextOptions) => {
blacklisted: waldo_user_data
? waldo_user_data.user.blacklisted
: false,
scope: payload.scope.split(" ")
};

return await createContextInner({
Expand Down
38 changes: 0 additions & 38 deletions apps/app/server/trpc/router/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,6 @@ import { router, protectedProcedure, publicProcedure } from '../trpc';
import * as Sentry from '@sentry/nextjs';

export const siteRouter = router({
getPageData: protectedProcedure
.meta({ openapi: { method: 'GET', path: '/site/page' } })
.input(
z
.object({
name: z.string(),
})
.transform(input => {
return {
name: serverSanitize(input.name),
};
}),
)
.output(
z.object({
maintenance: z.boolean(),
name: z.string(),
parentName: z.string(),
alertDescription: z.string().nullable(),
alertTitle: z.string().nullable(),
isCustomAlert: z.boolean(),
}),
)
.query(async ({ input, ctx }) => {
const pageData = await ctx.prisma.waldoPage.findFirst({
where: {
name: input.name,
},
});
if (pageData == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Waldo Vision Page not found in the database.',
});
}
// no error checking because the docs will never be deleted.
return pageData;
}),
getSiteData: publicProcedure
.meta({ openapi: { method: 'GET', path: '/site/site' } })
.input(
Expand Down
43 changes: 43 additions & 0 deletions apps/app/server/trpc/router/site/getPageData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { rbacProtectedProcedure } from '../../trpc';
import { z } from 'zod';
import { serverSanitize } from '@utils/sanitize';
import { TRPCError } from '@trpc/server';

export default rbacProtectedProcedure(["read:all", "read:pagemetadata"])
.meta({ openapi: { method: 'GET', path: '/site/page' } })
.input(
z
.object({
name: z.string(),
})
.transform(input => {
return {
name: serverSanitize(input.name),
};
}),
)
.output(
z.object({
maintenance: z.boolean(),
name: z.string(),
parentName: z.string(),
alertDescription: z.string().nullable(),
alertTitle: z.string().nullable(),
isCustomAlert: z.boolean(),
}),
)
.query(async ({ input, ctx }) => {
const pageData = await ctx.prisma.waldoPage.findFirst({
where: {
name: input.name,
},
});
if (pageData == null) {
throw new TRPCError({
code: 'NOT_FOUND',
message: 'Waldo Vision Page not found in the database.',
});
}
// no error checking because the docs will never be deleted.
return pageData;
});
7 changes: 7 additions & 0 deletions apps/app/server/trpc/router/site/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { router, protectedProcedure, publicProcedure } from '../../trpc';

import getPageData from './getPageData';

export const siteRouter = router({
getPageData
});
34 changes: 31 additions & 3 deletions apps/app/server/trpc/trpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export const publicProcedure = t.procedure.use(sentryMiddleware);
* users are logged in
* rate limit middleware
*/
const isAuthed = t.middleware(async ({ ctx, next }) => {
const isAuthed2 = t.middleware(async ({ ctx, next }) => {
if (!ctx.session || !ctx.session.logto_id)
throw new TRPCError({ code: 'UNAUTHORIZED' });

Expand All @@ -51,6 +51,27 @@ const isAuthed = t.middleware(async ({ ctx, next }) => {
});
});

const isAuthed = (requiredScope: Array<string>) => {
return t.middleware(async ({ctx, next}) => {
if (!ctx.session || !ctx.session.logto_id)
throw new TRPCError({ code: 'UNAUTHORIZED' });

const userScope = ctx.session.scope;
const userHasRequiredScope = requiredScope.some(a => userScope.includes(a));
if(!userHasRequiredScope)
throw new TRPCError({ code: 'UNAUTHORIZED' });

// ?set the user on the sentry scope
// ?so we can track effected users
Sentry.getCurrentHub().getScope().setUser({ id: ctx.session.logto_id });
return next({
ctx: {
session: {...ctx.session, user: ctx.session}
}
})
});
}

/**
* Protected procedure
**/
Expand Down Expand Up @@ -114,5 +135,12 @@ const isApiAuthed = t.middleware(async ({ ctx, next }) => {
export const apiProcedure = t.procedure.use(sentryMiddleware).use(isApiAuthed);

export const protectedProcedure = t.procedure
.use(sentryMiddleware)
.use(isAuthed);
.use(sentryMiddleware)
.use(isAuthed2);


export const rbacProtectedProcedure = (scope: Array<string>) => {
return t.procedure
.use(sentryMiddleware)
.use(isAuthed(scope));
}

0 comments on commit cbf2603

Please sign in to comment.