Skip to content

Commit 069415e

Browse files
committed
canAccessQuery now uses global flag and env var too
1 parent bc768cc commit 069415e

File tree

4 files changed

+74
-52
lines changed

4 files changed

+74
-52
lines changed

apps/webapp/app/env.server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1190,6 +1190,9 @@ const EnvironmentSchema = z
11901190
CLICKHOUSE_LOGS_DETAIL_MAX_THREADS: z.coerce.number().int().default(2),
11911191
CLICKHOUSE_LOGS_DETAIL_MAX_EXECUTION_TIME: z.coerce.number().int().default(60),
11921192

1193+
// Query feature flag
1194+
QUERY_FEATURE_ENABLED: z.string().default("1"),
1195+
11931196
// Query page ClickHouse limits (for TSQL queries)
11941197
QUERY_CLICKHOUSE_MAX_EXECUTION_TIME: z.coerce.number().int().default(10),
11951198
QUERY_CLICKHOUSE_MAX_MEMORY_USAGE: z.coerce.number().int().default(1_073_741_824), // 1GB in bytes

apps/webapp/app/presenters/OrganizationsPresenter.server.ts

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { RuntimeEnvironment, type PrismaClient } from "@trigger.dev/database";
1+
import type { RuntimeEnvironment, PrismaClient } from "@trigger.dev/database";
22
import { redirect } from "remix-typedjson";
33
import { prisma } from "~/db.server";
44
import { logger } from "~/services/logger.server";
@@ -10,7 +10,7 @@ import {
1010
} from "./SelectBestEnvironmentPresenter.server";
1111
import { sortEnvironments } from "~/utils/environmentSort";
1212
import { defaultAvatar, parseAvatar } from "~/components/primitives/Avatar";
13-
import { validatePartialFeatureFlags } from "~/v3/featureFlags.server";
13+
import { flags, validatePartialFeatureFlags } from "~/v3/featureFlags.server";
1414

1515
export class OrganizationsPresenter {
1616
#prismaClient: PrismaClient;
@@ -153,18 +153,24 @@ export class OrganizationsPresenter {
153153
},
154154
});
155155

156+
// Get global feature flags (no overrides or defaults)
157+
const globalFlags = await flags();
158+
156159
return orgs.map((org) => {
157-
const flagsResult = org.featureFlags
160+
const orgFlagsResult = org.featureFlags
158161
? validatePartialFeatureFlags(org.featureFlags as Record<string, unknown>)
159162
: ({ success: false } as const);
160-
const flags = flagsResult.success ? flagsResult.data : {};
163+
const orgFlags = orgFlagsResult.success ? orgFlagsResult.data : {};
164+
165+
// Combine global flags with org flags (org flags win)
166+
const combinedFlags = { ...globalFlags, ...orgFlags };
161167

162168
return {
163169
id: org.id,
164170
slug: org.slug,
165171
title: org.title,
166172
avatar: parseAvatar(org.avatar, defaultAvatar),
167-
featureFlags: flags,
173+
featureFlags: combinedFlags,
168174
projects: org.projects.map((project) => ({
169175
id: project.id,
170176
slug: project.slug,

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/route.tsx

Lines changed: 13 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ import { executeQuery, type QueryScope } from "~/services/queryService.server";
7575
import { requireUser } from "~/services/session.server";
7676
import { downloadFile, rowsToCSV, rowsToJSON } from "~/utils/dataExport";
7777
import { EnvironmentParamSchema, organizationBillingPath } from "~/utils/pathBuilder";
78-
import { FEATURE_FLAG, validateFeatureFlagValue } from "~/v3/featureFlags.server";
78+
import { canAccessQuery } from "~/v3/canAccessQuery.server";
7979
import { querySchemas } from "~/v3/querySchemas";
8080
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
8181
import { QueryHelpSidebar } from "./QueryHelpSidebar";
@@ -91,40 +91,6 @@ function toISOString(value: Date | string): string {
9191
return value.toISOString();
9292
}
9393

94-
async function hasQueryAccess(
95-
userId: string,
96-
isAdmin: boolean,
97-
isImpersonating: boolean,
98-
organizationSlug: string
99-
): Promise<boolean> {
100-
if (isAdmin || isImpersonating) {
101-
return true;
102-
}
103-
104-
// Check organization feature flags
105-
const organization = await prisma.organization.findFirst({
106-
where: {
107-
slug: organizationSlug,
108-
members: { some: { userId } },
109-
},
110-
select: {
111-
featureFlags: true,
112-
},
113-
});
114-
115-
if (!organization?.featureFlags) {
116-
return false;
117-
}
118-
119-
const flags = organization.featureFlags as Record<string, unknown>;
120-
const hasQueryAccessResult = validateFeatureFlagValue(
121-
FEATURE_FLAG.hasQueryAccess,
122-
flags.hasQueryAccess
123-
);
124-
125-
return hasQueryAccessResult.success && hasQueryAccessResult.data === true;
126-
}
127-
12894
const scopeOptions = [
12995
{ value: "environment", label: "Environment" },
13096
{ value: "project", label: "Project" },
@@ -135,12 +101,12 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => {
135101
const user = await requireUser(request);
136102
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
137103

138-
const canAccess = await hasQueryAccess(
139-
user.id,
140-
user.admin,
141-
user.isImpersonating,
142-
organizationSlug
143-
);
104+
const canAccess = await canAccessQuery({
105+
userId: user.id,
106+
isAdmin: user.admin,
107+
isImpersonating: user.isImpersonating,
108+
organizationSlug,
109+
});
144110
if (!canAccess) {
145111
throw redirect("/");
146112
}
@@ -200,12 +166,12 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
200166
const user = await requireUser(request);
201167
const { projectParam, organizationSlug, envParam } = EnvironmentParamSchema.parse(params);
202168

203-
const canAccess = await hasQueryAccess(
204-
user.id,
205-
user.admin,
206-
user.isImpersonating,
207-
organizationSlug
208-
);
169+
const canAccess = await canAccessQuery({
170+
userId: user.id,
171+
isAdmin: user.admin,
172+
isImpersonating: user.isImpersonating,
173+
organizationSlug,
174+
});
209175
if (!canAccess) {
210176
return typedjson(
211177
{
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { prisma } from "~/db.server";
2+
import { env } from "~/env.server";
3+
import { FEATURE_FLAG, makeFlag } from "~/v3/featureFlags.server";
4+
5+
export async function canAccessQuery(options: {
6+
userId: string;
7+
isAdmin: boolean;
8+
isImpersonating: boolean;
9+
organizationSlug: string;
10+
}): Promise<boolean> {
11+
const { userId, isAdmin, isImpersonating, organizationSlug } = options;
12+
13+
// 1. If it's on then we have access
14+
const globallyEnabled = env.QUERY_FEATURE_ENABLED === "1";
15+
if (globallyEnabled) {
16+
return true;
17+
}
18+
19+
// 2. Admins always have access
20+
if (isAdmin || isImpersonating) {
21+
return true;
22+
}
23+
24+
// 3. Check if org/global feature flag is on
25+
const org = await prisma.organization.findFirst({
26+
where: {
27+
slug: organizationSlug,
28+
members: { some: { userId } },
29+
},
30+
select: {
31+
featureFlags: true,
32+
},
33+
});
34+
35+
const flag = makeFlag();
36+
const flagResult = await flag({
37+
key: FEATURE_FLAG.hasQueryAccess,
38+
defaultValue: false,
39+
overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
40+
});
41+
if (flagResult) {
42+
return true;
43+
}
44+
45+
// 4. Not enabled anywhere
46+
return false;
47+
}

0 commit comments

Comments
 (0)