Skip to content

Commit

Permalink
Restrict stage visibility (#856)
Browse files Browse the repository at this point in the history
* initial query

* Pass userId to getStages

* Add todo

* Add check to getStage query

* Pass userId to stage API routes

* Fix conflicting user IDs

* Update stage test payload

* Fix import

* Remove redirect to settings now that we have pub and stage permissions

* Keep move constraints but make stage show up

* Edit move constraints while building out workflow

* Revert "Edit move constraints while building out workflow"

This reverts commit d986381.

* Move condition for when stage is undefined in createStageList

* Fix authorization and update test
  • Loading branch information
allisonking authored Jan 14, 2025
1 parent b114bd2 commit 4ab09a8
Show file tree
Hide file tree
Showing 18 changed files with 267 additions and 70 deletions.
14 changes: 10 additions & 4 deletions core/app/api/v0/c/[communitySlug]/site/[...ts-rest]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ const handler = createNextHandler(
},
stages: {
get: async (req) => {
const { community } = await checkAuthorization({
const { user } = await checkAuthorization({
token: { scope: ApiAccessScope.stage, type: ApiAccessType.read },
cookies: {
capability: Capabilities.viewStage,
Expand All @@ -610,7 +610,10 @@ const handler = createNextHandler(
},
},
});
const stage = await getStage(req.params.stageId as StagesId).executeTakeFirst();
const stage = await getStage(
req.params.stageId as StagesId,
user.id
).executeTakeFirst();
if (!stage) {
throw new NotFoundError("No stage found");
}
Expand All @@ -621,12 +624,15 @@ const handler = createNextHandler(
};
},
getMany: async (req, res) => {
const { community } = await checkAuthorization({
const { community, user } = await checkAuthorization({
token: { scope: ApiAccessScope.stage, type: ApiAccessType.read },
cookies: false,
});

const stages = await getStages({ communityId: community.id }).execute();
const stages = await getStages({
communityId: community.id,
userId: user.id,
}).execute();
return {
status: 200,
body: stages,
Expand Down
4 changes: 1 addition & 3 deletions core/app/c/[communitySlug]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ export default async function MainLayout({ children, params }: Props) {

const role = getCommunityRole(user, community);

if (role === "contributor" || !role) {
// TODO: allow contributors to view /c/* pages after we implement membership and
// role-based authorization checks
if (!role) {
redirect("/settings");
}

Expand Down
5 changes: 4 additions & 1 deletion core/app/c/[communitySlug]/pubs/[pubId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ export default async function Page({
);

const communityMembersPromise = selectCommunityMembers({ communityId: community.id }).execute();
const communityStagesPromise = getStages({ communityId: community.id }).execute();
const communityStagesPromise = getStages({
communityId: community.id,
userId: user.id,
}).execute();

// We don't pass the userId here because we want to include related pubs regardless of authorization
// This is safe because we've already explicitly checked authorization for the root pub
Expand Down
4 changes: 2 additions & 2 deletions core/app/c/[communitySlug]/settings/tokens/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ export const metadata: Metadata = {
};

export default async function Page({ params }: { params: { communitySlug: string } }) {
await getPageLoginData();
const { user } = await getPageLoginData();
const community = await findCommunityBySlug(params.communitySlug);
if (!community) {
return notFound();
}

const [stages, existingTokens] = await Promise.all([
getStages({ communityId: community.id }).execute(),
getStages({ communityId: community.id, userId: user.id }).execute(),
getApiAccessTokensByCommunity(community.id).execute(),
]);

Expand Down
15 changes: 9 additions & 6 deletions core/app/c/[communitySlug]/stages/[stageId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { cache, Suspense } from "react";
import Link from "next/link";
import { notFound } from "next/navigation";

import type { CommunitiesId, StagesId } from "db/public";
import type { CommunitiesId, StagesId, UsersId } from "db/public";
import { Capabilities, MembershipType } from "db/public";
import { Button } from "ui/button";

Expand All @@ -16,20 +16,23 @@ import { getStages } from "~/lib/server/stages";
import { PubListSkeleton } from "../../pubs/PubList";
import { StagePubs } from "../components/StageList";

const getStageCached = cache(async (stageId: StagesId, communityId: CommunitiesId) => {
return getStages({ stageId, communityId }).executeTakeFirst();
});
const getStageCached = cache(
async (stageId: StagesId, communityId: CommunitiesId, userId: UsersId) => {
return getStages({ stageId, communityId, userId }).executeTakeFirst();
}
);

export async function generateMetadata({
params: { stageId, communitySlug },
}: {
params: { stageId: StagesId; communitySlug: string };
}): Promise<Metadata> {
const { user } = await getPageLoginData();
const community = await findCommunityBySlug(communitySlug);
if (!community) {
notFound();
}
const stage = await getStageCached(stageId, community.id);
const stage = await getStageCached(stageId, community.id, user.id);
if (!stage) {
notFound();
}
Expand Down Expand Up @@ -57,7 +60,7 @@ export default async function Page({

const page = searchParams.page ? parseInt(searchParams.page) : 1;

const stagePromise = getStageCached(stageId, community.id);
const stagePromise = getStageCached(stageId, community.id, user.id);
const capabilityPromise = userCan(
Capabilities.editCommunity,
{ type: MembershipType.community, communityId: community.id },
Expand Down
6 changes: 3 additions & 3 deletions core/app/c/[communitySlug]/stages/components/StageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ import { PubListSkeleton } from "../../pubs/PubList";
import { StagePubActions } from "./StagePubActions";

type Props = {
userId: UsersId;
communityId: CommunitiesId;
pageContext: PageContext;
userId: UsersId;
};

export async function StageList(props: Props) {
const { communityId } = props;
const { communityId, userId } = props;
const [communityStages, communityMembers] = await Promise.all([
getStages({ communityId }).execute(),
getStages({ communityId, userId }).execute(),
selectCommunityMembers({ communityId }).execute(),
]);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const StagePanel = async (props: Props) => {
let open = Boolean(props.stageId);

if (props.stageId) {
const stage = await getStage(props.stageId).executeTakeFirst();
const stage = await getStage(props.stageId, props.user.id).executeTakeFirst();
if (stage === null) {
open = false;
}
Expand All @@ -38,17 +38,22 @@ export const StagePanel = async (props: Props) => {
<TabsTrigger value="members">Members</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<StagePanelOverview stageId={props.stageId} />
<StagePanelOverview stageId={props.stageId} userId={props.user.id} />
</TabsContent>
<TabsContent value="pubs">
<StagePanelPubs
stageId={props.stageId as StagesId}
pageContext={props.pageContext}
userId={props.user.id}
/>
</TabsContent>
<TabsContent value="actions" className="space-y-2">
<StagePanelActions stageId={props.stageId} pageContext={props.pageContext} />
<StagePanelRules stageId={props.stageId} />
<StagePanelActions
stageId={props.stageId}
pageContext={props.pageContext}
userId={props.user.id}
/>
<StagePanelRules stageId={props.stageId} userId={props.user.id} />
</TabsContent>
<TabsContent value="members">
<StagePanelMembers stageId={props.stageId} user={props.user} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Suspense } from "react";

import type { StagesId } from "db/public";
import type { StagesId, UsersId } from "db/public";
import { Card, CardContent } from "ui/card";
import { Separator } from "ui/separator";

Expand All @@ -13,11 +13,12 @@ import { StagePanelOverviewManagement } from "./StagePanelOverviewManagement";

type PropsInner = {
stageId: StagesId;
userId: UsersId;
};

const StagePanelOverviewInner = async (props: PropsInner) => {
const [stage, communitySlug] = await Promise.all([
getStage(props.stageId).executeTakeFirst(),
getStage(props.stageId, props.userId).executeTakeFirst(),
getCommunitySlug(),
]);

Expand Down Expand Up @@ -47,6 +48,7 @@ const StagePanelOverviewInner = async (props: PropsInner) => {

type Props = {
stageId: string | undefined;
userId: UsersId;
};

export const StagePanelOverview = async (props: Props) => {
Expand All @@ -56,7 +58,7 @@ export const StagePanelOverview = async (props: Props) => {

return (
<Suspense fallback={<SkeletonCard />}>
<StagePanelOverviewInner stageId={props.stageId as StagesId} />
<StagePanelOverviewInner stageId={props.stageId as StagesId} userId={props.userId} />
</Suspense>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Suspense } from "react";
import Link from "next/link";

import type { PubsId, StagesId } from "db/public";
import type { PubsId, StagesId, UsersId } from "db/public";
import { Card, CardContent } from "ui/card";

import type { PageContext } from "~/app/components/ActionUI/PubsRunActionDropDownMenu";
Expand All @@ -16,13 +16,14 @@ import { getCommunitySlug } from "~/lib/server/cache/getCommunitySlug";
type PropsInner = {
stageId: StagesId;
pageContext: PageContext;
userId: UsersId;
};

const StagePanelPubsInner = async (props: PropsInner) => {
const [stagePubs, stageActionInstances, stage] = await Promise.all([
getStagePubs(props.stageId).execute(),
getStageActions(props.stageId).execute(),
getStage(props.stageId).executeTakeFirst(),
getStage(props.stageId, props.userId).executeTakeFirst(),
]);
const communitySlug = getCommunitySlug();

Expand Down Expand Up @@ -69,6 +70,7 @@ const StagePanelPubsInner = async (props: PropsInner) => {
type Props = {
stageId?: StagesId;
pageContext: PageContext;
userId: UsersId;
};

export const StagePanelPubs = async (props: Props) => {
Expand All @@ -78,7 +80,11 @@ export const StagePanelPubs = async (props: Props) => {

return (
<Suspense fallback={<SkeletonCard />}>
<StagePanelPubsInner stageId={props.stageId} pageContext={props.pageContext} />
<StagePanelPubsInner
stageId={props.stageId}
pageContext={props.pageContext}
userId={props.userId}
/>
</Suspense>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Suspense } from "react";

import type { StagesId } from "db/public";
import type { StagesId, UsersId } from "db/public";
import { Card, CardContent } from "ui/card";

import type { PageContext } from "~/app/components/ActionUI/PubsRunActionDropDownMenu";
Expand All @@ -15,11 +15,12 @@ import { StagePanelActionEditor } from "./StagePanelActionEditor";
type PropsInner = {
stageId: StagesId;
pageContext: PageContext;
userId: UsersId;
};

const StagePanelActionsInner = async (props: PropsInner) => {
const [stage, actionInstances] = await Promise.all([
getStage(props.stageId).executeTakeFirst(),
getStage(props.stageId, props.userId).executeTakeFirst(),
getStageActions(props.stageId).execute(),
]);

Expand Down Expand Up @@ -74,6 +75,7 @@ const StagePanelActionsInner = async (props: PropsInner) => {
type Props = {
stageId?: StagesId;
pageContext: PageContext;
userId: UsersId;
};

export const StagePanelActions = async (props: Props) => {
Expand All @@ -83,7 +85,11 @@ export const StagePanelActions = async (props: Props) => {

return (
<Suspense fallback={<SkeletonCard />}>
<StagePanelActionsInner stageId={props.stageId} pageContext={props.pageContext} />
<StagePanelActionsInner
stageId={props.stageId}
pageContext={props.pageContext}
userId={props.userId}
/>
</Suspense>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Suspense } from "react";

import type { CommunitiesId, StagesId } from "db/public";
import type { CommunitiesId, StagesId, UsersId } from "db/public";
import { Card, CardContent } from "ui/card";

import { SkeletonCard } from "~/app/components/skeletons/SkeletonCard";
Expand All @@ -10,11 +10,12 @@ import { StagePanelRuleCreator } from "./StagePanelRuleCreator";

type PropsInner = {
stageId: StagesId;
userId: UsersId;
};

const StagePanelRulesInner = async (props: PropsInner) => {
const [stage, actionInstances, rules] = await Promise.all([
getStage(props.stageId).executeTakeFirst(),
getStage(props.stageId, props.userId).executeTakeFirst(),
getStageActions(props.stageId).execute(),
getStageRules(props.stageId).execute(),
]);
Expand Down Expand Up @@ -66,6 +67,7 @@ const StagePanelRulesInner = async (props: PropsInner) => {

type Props = {
stageId?: StagesId;
userId: UsersId;
};

export const StagePanelRules = async (props: Props) => {
Expand All @@ -75,7 +77,7 @@ export const StagePanelRules = async (props: Props) => {

return (
<Suspense fallback={<SkeletonCard />}>
<StagePanelRulesInner stageId={props.stageId} />
<StagePanelRulesInner stageId={props.stageId} userId={props.userId} />
</Suspense>
);
};
6 changes: 4 additions & 2 deletions core/app/c/[communitySlug]/stages/manage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export async function generateMetadata({
return { title: "Workflow Editor" };
}

const stage = await getStage(editingStageId as StagesId).executeTakeFirst();
const { user } = await getPageLoginData();

const stage = await getStage(editingStageId as StagesId, user.id).executeTakeFirst();

if (!stage) {
return { title: "Stage" };
Expand All @@ -65,7 +67,7 @@ export default async function Page({ params, searchParams }: Props) {
redirect(`/c/${params.communitySlug}/unauthorized`);
}

const stages = await getStages({ communityId: community.id }).execute();
const stages = await getStages({ communityId: community.id, userId: user.id }).execute();

const pageContext = {
params,
Expand Down
7 changes: 5 additions & 2 deletions core/lib/db/queries.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
import { cache } from "react";
import { jsonObjectFrom } from "kysely/helpers/postgres";

import type { StagesId } from "db/public";
import type { StagesId, UsersId } from "db/public";

import type { RuleConfig } from "~/actions/types";
import { db } from "~/kysely/database";
import prisma from "~/prisma/db";
import { pubType, pubValuesByRef } from "../server";
import { communityMemberInclude, stageInclude } from "../server/_legacy-integration-queries";
import { autoCache } from "../server/cache/autoCache";
import { viewableStagesCte } from "../server/stages";
import { SAFE_USER_SELECT } from "../server/user";

export const getStage = cache((stageId: StagesId) => {
export const getStage = cache((stageId: StagesId, userId: UsersId) => {
return autoCache(
db
.with("viewableStages", (db) => viewableStagesCte({ db, userId }))
.selectFrom("stages")
.innerJoin("viewableStages", "viewableStages.stageId", "stages.id")
.select([
"stages.id",
"communityId",
Expand Down
Loading

0 comments on commit 4ab09a8

Please sign in to comment.