fix: read-after-write issues (#215)
* fix: read-after-write issues * fix: coderabbit comments * fix: clear cache on invite * fix: use primary after a read
This commit is contained in:
committed by
GitHub
parent
abacf66155
commit
f454449365
@@ -1,93 +1,5 @@
|
||||
import { db, getProjectById } from '@openpanel/db';
|
||||
import { cacheable } from '@openpanel/redis';
|
||||
|
||||
export const getProjectAccessCached = cacheable(getProjectAccess, 60 * 5);
|
||||
export async function getProjectAccess({
|
||||
userId,
|
||||
projectId,
|
||||
}: {
|
||||
userId: string;
|
||||
projectId: string;
|
||||
}) {
|
||||
try {
|
||||
// Check if user has access to the project
|
||||
const project = await getProjectById(projectId);
|
||||
if (!project?.organizationId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [projectAccess, member] = await Promise.all([
|
||||
db.projectAccess.findMany({
|
||||
where: {
|
||||
userId,
|
||||
organizationId: project.organizationId,
|
||||
},
|
||||
}),
|
||||
db.member.findFirst({
|
||||
where: {
|
||||
organizationId: project.organizationId,
|
||||
userId,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (projectAccess.length === 0 && member) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return projectAccess.find((item) => item.projectId === projectId);
|
||||
} catch (err) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export const getOrganizationAccessCached = cacheable(
|
||||
export {
|
||||
getOrganizationAccess,
|
||||
60 * 5,
|
||||
);
|
||||
export async function getOrganizationAccess({
|
||||
userId,
|
||||
organizationId,
|
||||
}: {
|
||||
userId: string;
|
||||
organizationId: string;
|
||||
}) {
|
||||
return db.member.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
organizationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const getClientAccessCached = cacheable(getClientAccess, 60 * 5);
|
||||
export async function getClientAccess({
|
||||
userId,
|
||||
clientId,
|
||||
}: {
|
||||
userId: string;
|
||||
clientId: string;
|
||||
}) {
|
||||
const client = await db.client.findFirst({
|
||||
where: {
|
||||
id: clientId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!client) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (client.projectId) {
|
||||
return getProjectAccess({ userId, projectId: client.projectId });
|
||||
}
|
||||
|
||||
if (client.organizationId) {
|
||||
return getOrganizationAccess({
|
||||
userId,
|
||||
organizationId: client.organizationId,
|
||||
});
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
getProjectAccess,
|
||||
getClientAccess,
|
||||
} from '@openpanel/db';
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
getUserAccount,
|
||||
} from '@openpanel/db';
|
||||
import { sendEmail } from '@openpanel/email';
|
||||
import { deleteCache } from '@openpanel/redis';
|
||||
import {
|
||||
zRequestResetPassword,
|
||||
zResetPassword,
|
||||
@@ -74,6 +75,7 @@ export const authRouter = createTRPCRouter({
|
||||
deleteSessionTokenCookie(ctx.setCookie);
|
||||
if (ctx.session?.session?.id) {
|
||||
await invalidateSession(ctx.session.session.id);
|
||||
await deleteCache(`validateSession:${ctx.session.session.id}`);
|
||||
}
|
||||
}),
|
||||
signInOAuth: publicProcedure
|
||||
@@ -333,6 +335,7 @@ export const authRouter = createTRPCRouter({
|
||||
const session = await validateSessionToken(token);
|
||||
|
||||
if (session.session) {
|
||||
await deleteCache(`validateSession:${session.session.id}`);
|
||||
// Re-set the cookie with updated expiration
|
||||
setSessionTokenCookie(ctx.setCookie, token, session.session.expiresAt);
|
||||
return {
|
||||
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
differenceInWeeks,
|
||||
formatISO,
|
||||
} from 'date-fns';
|
||||
import { getProjectAccessCached } from '../access';
|
||||
import { getProjectAccess } from '../access';
|
||||
import { TRPCAccessError } from '../errors';
|
||||
import {
|
||||
cacheMiddleware,
|
||||
@@ -367,7 +367,7 @@ export const chartRouter = createTRPCRouter({
|
||||
.input(zChartInput)
|
||||
.query(async ({ input, ctx }) => {
|
||||
if (ctx.session.userId) {
|
||||
const access = await getProjectAccessCached({
|
||||
const access = await getProjectAccess({
|
||||
projectId: input.projectId,
|
||||
userId: ctx.session.userId,
|
||||
});
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from '@openpanel/validation';
|
||||
|
||||
import { clone } from 'ramda';
|
||||
import { getProjectAccessCached } from '../access';
|
||||
import { getProjectAccess } from '../access';
|
||||
import { TRPCAccessError } from '../errors';
|
||||
import { createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
|
||||
|
||||
@@ -266,7 +266,7 @@ export const eventRouter = createTRPCRouter({
|
||||
)
|
||||
.query(async ({ input: { projectId, cursor, limit }, ctx }) => {
|
||||
if (ctx.session.userId) {
|
||||
const access = await getProjectAccessCached({
|
||||
const access = await getProjectAccess({
|
||||
projectId,
|
||||
userId: ctx.session.userId,
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
zCreateSlackIntegration,
|
||||
zCreateWebhookIntegration,
|
||||
} from '@openpanel/validation';
|
||||
import { getOrganizationAccessCached } from '../access';
|
||||
import { getOrganizationAccess } from '../access';
|
||||
import { TRPCAccessError } from '../errors';
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
|
||||
@@ -23,7 +23,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const access = await getOrganizationAccessCached({
|
||||
const access = await getOrganizationAccess({
|
||||
userId: ctx.session.userId,
|
||||
organizationId: integration.organizationId,
|
||||
});
|
||||
@@ -122,7 +122,7 @@ export const integrationRouter = createTRPCRouter({
|
||||
},
|
||||
});
|
||||
|
||||
const access = await getOrganizationAccessCached({
|
||||
const access = await getOrganizationAccess({
|
||||
userId: ctx.session.userId,
|
||||
organizationId: integration.organizationId,
|
||||
});
|
||||
|
||||
@@ -4,18 +4,15 @@ import { has } from 'ramda';
|
||||
import superjson from 'superjson';
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
import {
|
||||
COOKIE_OPTIONS,
|
||||
EMPTY_SESSION,
|
||||
validateSessionToken,
|
||||
} from '@openpanel/auth';
|
||||
import { getCache, getRedisCache } from '@openpanel/redis';
|
||||
import { COOKIE_OPTIONS, type SessionValidationResult } from '@openpanel/auth';
|
||||
import { runWithAlsSession } from '@openpanel/db';
|
||||
import { getRedisCache } from '@openpanel/redis';
|
||||
import type { ISetCookie } from '@openpanel/validation';
|
||||
import {
|
||||
createTrpcRedisLimiter,
|
||||
defaultFingerPrint,
|
||||
} from '@trpc-limiter/redis';
|
||||
import { getOrganizationAccessCached, getProjectAccessCached } from './access';
|
||||
import { getOrganizationAccess, getProjectAccess } from './access';
|
||||
import { TRPCAccessError } from './errors';
|
||||
|
||||
export const rateLimitMiddleware = ({
|
||||
@@ -44,10 +41,6 @@ export async function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||
});
|
||||
};
|
||||
|
||||
const session = cookies?.session
|
||||
? await validateSessionToken(cookies.session!)
|
||||
: EMPTY_SESSION;
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
await new Promise((res) =>
|
||||
setTimeout(() => res(1), Math.min(Math.random() * 500, 200)),
|
||||
@@ -57,7 +50,7 @@ export async function createContext({ req, res }: CreateFastifyContextOptions) {
|
||||
return {
|
||||
req,
|
||||
res,
|
||||
session,
|
||||
session: (req as any).session as SessionValidationResult,
|
||||
// we do not get types for `setCookie` from fastify
|
||||
// so define it here and be safe in routers
|
||||
setCookie,
|
||||
@@ -102,37 +95,39 @@ const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
||||
|
||||
// Only used on protected routes
|
||||
const enforceAccess = t.middleware(async ({ ctx, next, type, getRawInput }) => {
|
||||
const rawInput = await getRawInput();
|
||||
if (type === 'mutation' && process.env.DEMO_USER_ID) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'You are not allowed to do this in demo mode',
|
||||
});
|
||||
}
|
||||
|
||||
if (has('projectId', rawInput)) {
|
||||
const access = await getProjectAccessCached({
|
||||
userId: ctx.session.userId!,
|
||||
projectId: rawInput.projectId as string,
|
||||
});
|
||||
|
||||
if (!access) {
|
||||
throw TRPCAccessError('You do not have access to this project');
|
||||
return runWithAlsSession(ctx.session.session?.id, async () => {
|
||||
const rawInput = await getRawInput();
|
||||
if (type === 'mutation' && process.env.DEMO_USER_ID) {
|
||||
throw new TRPCError({
|
||||
code: 'UNAUTHORIZED',
|
||||
message: 'You are not allowed to do this in demo mode',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (has('organizationId', rawInput)) {
|
||||
const access = await getOrganizationAccessCached({
|
||||
userId: ctx.session.userId!,
|
||||
organizationId: rawInput.organizationId as string,
|
||||
});
|
||||
if (has('projectId', rawInput)) {
|
||||
const access = await getProjectAccess({
|
||||
userId: ctx.session.userId!,
|
||||
projectId: rawInput.projectId as string,
|
||||
});
|
||||
|
||||
if (!access) {
|
||||
throw TRPCAccessError('You do not have access to this organization');
|
||||
if (!access) {
|
||||
throw TRPCAccessError('You do not have access to this project');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
if (has('organizationId', rawInput)) {
|
||||
const access = await getOrganizationAccess({
|
||||
userId: ctx.session.userId!,
|
||||
organizationId: rawInput.organizationId as string,
|
||||
});
|
||||
|
||||
if (!access) {
|
||||
throw TRPCAccessError('You do not have access to this organization');
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
export const createTRPCRouter = t.router;
|
||||
@@ -157,11 +152,21 @@ const loggerMiddleware = t.middleware(
|
||||
},
|
||||
);
|
||||
|
||||
export const publicProcedure = t.procedure.use(loggerMiddleware);
|
||||
const sessionScopeMiddleware = t.middleware(async ({ ctx, next }) => {
|
||||
const sessionId = ctx.session.session?.id ?? null;
|
||||
return runWithAlsSession(sessionId, async () => {
|
||||
return next();
|
||||
});
|
||||
});
|
||||
|
||||
export const publicProcedure = t.procedure
|
||||
.use(loggerMiddleware)
|
||||
.use(sessionScopeMiddleware);
|
||||
export const protectedProcedure = t.procedure
|
||||
.use(enforceUserIsAuthed)
|
||||
.use(enforceAccess)
|
||||
.use(loggerMiddleware);
|
||||
.use(loggerMiddleware)
|
||||
.use(sessionScopeMiddleware);
|
||||
|
||||
const middlewareMarker = 'middlewareMarker' as 'middlewareMarker' & {
|
||||
__brand: 'middlewareMarker';
|
||||
|
||||
Reference in New Issue
Block a user