-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(webapp): Org level feature flags for Private Links #3287
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,7 +13,7 @@ import { Header2 } from "~/components/primitives/Headers"; | |
| import { NavBar, PageAccessories, PageTitle } from "~/components/primitives/PageHeader"; | ||
| import { Paragraph } from "~/components/primitives/Paragraph"; | ||
| import { prisma } from "~/db.server"; | ||
| import { featuresForRequest } from "~/features.server"; | ||
| import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server"; | ||
| import { logger } from "~/services/logger.server"; | ||
| import { getPrivateLinks } from "~/services/platform.v3.server"; | ||
| import { requireUserId } from "~/services/session.server"; | ||
|
|
@@ -44,8 +44,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) { | |
| const userId = await requireUserId(request); | ||
| const { organizationSlug } = OrganizationParamsSchema.parse(params); | ||
|
|
||
| const { hasPrivateConnections } = featuresForRequest(request); | ||
| if (!hasPrivateConnections) { | ||
| const canAccess = await canAccessPrivateConnections({ organizationSlug, userId }); | ||
| if (!canAccess) { | ||
| return redirect(organizationPath({ slug: organizationSlug })); | ||
|
Comment on lines
+47
to
49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard the action with the same Line 47 gates only the loader. The delete action (Line 71 onward) is still callable directly and can mutate state without feature access, which weakens enforcement of the org-level flag. 🔧 Suggested fix export const action = async ({ request, params }: ActionFunctionArgs) => {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
+ const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
+
+ if (!canAccess) {
+ return redirect(organizationPath({ slug: organizationSlug }));
+ }
if (request.method !== "DELETE" && request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}Also applies to: 71-113 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,7 +24,7 @@ import { Paragraph } from "~/components/primitives/Paragraph"; | |
| import { Select, SelectItem } from "~/components/primitives/Select"; | ||
| import { prisma } from "~/db.server"; | ||
| import { env } from "~/env.server"; | ||
| import { featuresForRequest } from "~/features.server"; | ||
| import { canAccessPrivateConnections } from "~/v3/canAccessPrivateConnections.server"; | ||
| import { | ||
| redirectWithErrorMessage, | ||
| redirectWithSuccessMessage, | ||
|
|
@@ -56,8 +56,8 @@ export async function loader({ params, request }: LoaderFunctionArgs) { | |
| const userId = await requireUserId(request); | ||
| const { organizationSlug } = OrganizationParamsSchema.parse(params); | ||
|
|
||
| const { hasPrivateConnections } = featuresForRequest(request); | ||
| if (!hasPrivateConnections) { | ||
| const canAccess = await canAccessPrivateConnections({ organizationSlug, userId }); | ||
| if (!canAccess) { | ||
| return redirect(organizationPath({ slug: organizationSlug })); | ||
| } | ||
|
Comment on lines
+59
to
62
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Apply the same access gate in The loader is protected, but create mutation in Line 95+ is still reachable via direct form POST without feature access validation. 🔧 Suggested fix export const action: ActionFunction = async ({ request, params }) => {
const userId = await requireUserId(request);
const { organizationSlug } = OrganizationParamsSchema.parse(params);
+ const canAccess = await canAccessPrivateConnections({ organizationSlug, userId });
+
+ if (!canAccess) {
+ return redirect(organizationPath({ slug: organizationSlug }));
+ }
const formData = await request.formData();
const submission = parse(formData, { schema });Also applies to: 95-154 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,27 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { prisma } from "~/db.server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { env } from "~/env.server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { FEATURE_FLAG, makeFlag } from "~/v3/featureFlags.server"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export async function canAccessPrivateConnections(options: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| organizationSlug: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId: string; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }): Promise<boolean> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { organizationSlug, userId } = options; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const org = await prisma.organization.findFirst({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| where: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| slug: organizationSlug, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| members: { some: { userId } }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| select: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| featureFlags: true, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const flag = makeFlag(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return flag({ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| key: FEATURE_FLAG.hasPrivateConnections, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| overrides: (org?.featureFlags as Record<string, unknown>) ?? {}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+22
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 Managed cloud requirement intentionally dropped for private connections The old Was this helpful? React with 👍 or 👎 to provide feedback.
Comment on lines
+11
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Return As written, this can return 🔧 Suggested fix const org = await prisma.organization.findFirst({
@@
});
+ if (!org) {
+ return false;
+ }
+
const flag = makeFlag();
return flag({
key: FEATURE_FLAG.hasPrivateConnections,
defaultValue: env.PRIVATE_CONNECTIONS_ENABLED === "1",
- overrides: (org?.featureFlags as Record<string, unknown>) ?? {},
+ overrides: (org.featureFlags as Record<string, unknown>) ?? {},
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,6 +9,7 @@ export const FEATURE_FLAG = { | |
| hasLogsPageAccess: "hasLogsPageAccess", | ||
| hasAiAccess: "hasAiAccess", | ||
| hasAiModelsAccess: "hasAiModelsAccess", | ||
| hasPrivateConnections: "hasPrivateConnections", | ||
| } as const; | ||
|
|
||
| const FeatureFlagCatalog = { | ||
|
|
@@ -19,6 +20,7 @@ const FeatureFlagCatalog = { | |
| [FEATURE_FLAG.hasLogsPageAccess]: z.coerce.boolean(), | ||
| [FEATURE_FLAG.hasAiAccess]: z.coerce.boolean(), | ||
| [FEATURE_FLAG.hasAiModelsAccess]: z.coerce.boolean(), | ||
| [FEATURE_FLAG.hasPrivateConnections]: z.coerce.boolean(), | ||
| }; | ||
|
|
||
| type FeatureFlagKey = keyof typeof FeatureFlagCatalog; | ||
|
|
@@ -47,21 +49,23 @@ export function makeFlag(_prisma: PrismaClientOrTransaction = prisma) { | |
|
|
||
| const flagSchema = FeatureFlagCatalog[opts.key]; | ||
|
|
||
| if (opts.overrides?.[opts.key]) { | ||
| if (opts.overrides?.[opts.key] !== undefined) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚩 makeFlag override fix changes behavior for falsy org flag values across all callers The change from Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| const parsed = flagSchema.safeParse(opts.overrides[opts.key]); | ||
|
|
||
| if (parsed.success) { | ||
| return parsed.data; | ||
| } | ||
| } | ||
|
|
||
| const parsed = flagSchema.safeParse(value?.value); | ||
| if (value !== null) { | ||
| const parsed = flagSchema.safeParse(value.value); | ||
|
|
||
| if (!parsed.success) { | ||
| return opts.defaultValue; | ||
| if (parsed.success) { | ||
| return parsed.data; | ||
| } | ||
| } | ||
|
|
||
| return parsed.data; | ||
| return opts.defaultValue; | ||
| } | ||
|
|
||
| return flag; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don’t gate this sidebar item by feature flag at nav level.
This hides the menu entry and diverges from the project’s established pattern where sidebar items are always shown and authorization is enforced in route loaders.
🔧 Suggested fix
Also applies to: 52-53, 110-118
🤖 Prompt for AI Agents