Skip to content

Commit

Permalink
Secure server actions (#835)
Browse files Browse the repository at this point in the history
* Add auth checks to pub update and create

* Add auth test for create pub action

* Parameterize test

* Tests for update pub

* Secure move, assign, field actions

* Add more checks

* Remove .only

* Add form check for updating pubs

* A few more checks

* Add more checks to stage management actions

* Add editCommunity checks for form builder actions

* Add TODO for updating a pub via a form auth check
  • Loading branch information
allisonking authored Dec 16, 2024
1 parent e92a95a commit 76f44f2
Show file tree
Hide file tree
Showing 22 changed files with 798 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ export default async function FormPage({
<ExternalFormWrapper
pub={pubForForm}
elements={form.elements}
formSlug={form.slug}
isUpdating={isUpdating}
withAutoSave={isUpdating}
withButtonElements
Expand Down
70 changes: 70 additions & 0 deletions core/app/c/[communitySlug]/fields/actions.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"use server";

import type { CommunitiesId, CoreSchemaType, PubFieldsId } from "db/public";
import { Capabilities } from "db/src/public/Capabilities";
import { MembershipType } from "db/src/public/MembershipType";
import { logger } from "logger";

import { db } from "~/kysely/database";
import { isUniqueConstraintError } from "~/kysely/errors";
import { getLoginData } from "~/lib/authentication/loginData";
import { userCan } from "~/lib/authorization/capabilities";
import { ApiError } from "~/lib/server";
import { autoRevalidate } from "~/lib/server/cache/autoRevalidate";
import { findCommunityBySlug } from "~/lib/server/community";
import { defineServerAction } from "~/lib/server/defineServerAction";

export const createField = defineServerAction(async function createField({
Expand All @@ -21,6 +27,23 @@ export const createField = defineServerAction(async function createField({
communityId: CommunitiesId;
isRelation: boolean;
}) {
const loginData = await getLoginData();

if (!loginData || !loginData.user) {
return ApiError.NOT_LOGGED_IN;
}

const { user } = loginData;
const authorized = await userCan(
Capabilities.createPubField,
{ type: MembershipType.community, communityId },
user.id
);

if (!authorized) {
return ApiError.UNAUTHORIZED;
}

try {
await autoRevalidate(
db.insertInto("pub_fields").values({ name, slug, schemaName, communityId, isRelation })
Expand All @@ -41,6 +64,29 @@ export const updateFieldName = defineServerAction(async function updateFieldName
fieldId: string,
name: string
) {
const loginData = await getLoginData();

if (!loginData || !loginData.user) {
return ApiError.NOT_LOGGED_IN;
}

const community = await findCommunityBySlug();

if (!community) {
return ApiError.COMMUNITY_NOT_FOUND;
}

const { user } = loginData;
const authorized = await userCan(
Capabilities.editPubField,
{ type: MembershipType.community, communityId: community.id },
user.id
);

if (!authorized) {
return ApiError.UNAUTHORIZED;
}

try {
await autoRevalidate(
db
Expand All @@ -57,6 +103,30 @@ export const updateFieldName = defineServerAction(async function updateFieldName
});

export const archiveField = defineServerAction(async function archiveField(fieldId: string) {
const loginData = await getLoginData();

if (!loginData || !loginData.user) {
return ApiError.NOT_LOGGED_IN;
}

const community = await findCommunityBySlug();

if (!community) {
return ApiError.COMMUNITY_NOT_FOUND;
}

const { user } = loginData;

const authorized = await userCan(
Capabilities.archivePubField,
{ type: MembershipType.community, communityId: community.id },
user.id
);

if (!authorized) {
return ApiError.UNAUTHORIZED;
}

try {
await autoRevalidate(
db
Expand Down
5 changes: 3 additions & 2 deletions core/app/c/[communitySlug]/stages/components/Assign.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import React, { useCallback, useMemo } from "react";

import type { PubsId, UsersId } from "db/public";
import { Button } from "ui/button";
import {
Command,
Expand Down Expand Up @@ -42,7 +43,7 @@ export default function Assign(props: Props) {
const runAssign = useServerAction(assign);

const onAssign = useCallback(
async (pubId: string, userId?: string) => {
async (pubId: PubsId, userId?: UsersId) => {
const error = await runAssign(pubId, userId);
if (userId) {
const user = expect(users.find((user) => user.id === userId));
Expand Down Expand Up @@ -74,7 +75,7 @@ export default function Assign(props: Props) {
(value: string) => {
const userId = value === selectedUserId ? undefined : value;
setSelectedUserId(userId);
onAssign(props.pub.id, userId);
onAssign(props.pub.id, userId as UsersId);
setOpen(false);
},
[selectedUserId, props.pub.id, onAssign]
Expand Down
2 changes: 1 addition & 1 deletion core/app/c/[communitySlug]/stages/components/Move.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function Move(props: Props) {
const [isMoving, startTransition] = useTransition();
const runMove = useServerAction(move);

const onMove = async (pubId: string, sourceStageId: string, destStageId: string) => {
const onMove = async (pubId: PubsId, sourceStageId: StagesId, destStageId: StagesId) => {
const err = await runMove(pubId, sourceStageId, destStageId);

if (isClientException(err)) {
Expand Down
59 changes: 46 additions & 13 deletions core/app/c/[communitySlug]/stages/components/lib/actions.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,73 @@
"use server";

import type { PubsId, StagesId, UsersId } from "db/public";
import { Capabilities } from "db/src/public/Capabilities";
import { MembershipType } from "db/src/public/MembershipType";

import { db } from "~/kysely/database";
import { getLoginData } from "~/lib/authentication/loginData";
import { userCan } from "~/lib/authorization/capabilities";
import { ApiError } from "~/lib/server";
import { autoRevalidate } from "~/lib/server/cache/autoRevalidate";
import { defineServerAction } from "~/lib/server/defineServerAction";

export const move = defineServerAction(async function move(
pubId: string,
sourceStageId: string,
destinationStageId: string
pubId: PubsId,
sourceStageId: StagesId,
destinationStageId: StagesId
) {
const loginData = await getLoginData();
if (!loginData || !loginData.user) {
return ApiError.NOT_LOGGED_IN;
}

const { user } = loginData;

const authorized = await userCan(
Capabilities.movePub,
{ type: MembershipType.pub, pubId },
user.id
);

if (!authorized) {
return ApiError.UNAUTHORIZED;
}

try {
await autoRevalidate(
db
.with("removed_pubsInStages", (db) =>
db
.deleteFrom("PubsInStages")
.where("pubId", "=", pubId as PubsId)
.where("stageId", "=", sourceStageId as StagesId)
.where("pubId", "=", pubId)
.where("stageId", "=", sourceStageId)
)
.insertInto("PubsInStages")
.values([{ pubId: pubId as PubsId, stageId: destinationStageId as StagesId }])
.values([{ pubId: pubId, stageId: destinationStageId }])
).executeTakeFirstOrThrow();

// TODO: Remove this when the above query is replaced by an
// autoRevalidated kyseley query
// revalidateTagsForCommunity(["PubsInStages"]);
} catch {
return { error: "The Pub was not successully moved" };
}
});

export const assign = defineServerAction(async function assign(pubId: string, userId?: string) {
export const assign = defineServerAction(async function assign(pubId: PubsId, userId?: UsersId) {
const loginData = await getLoginData();
if (!loginData || !loginData.user) {
return ApiError.NOT_LOGGED_IN;
}

const { user } = loginData;

const authorized = await userCan(
Capabilities.addPubMember,
{ type: MembershipType.pub, pubId },
user.id
);

if (!authorized) {
return ApiError.UNAUTHORIZED;
}

try {
await autoRevalidate(
db
Expand All @@ -42,8 +77,6 @@ export const assign = defineServerAction(async function assign(pubId: string, us
assigneeId: userId ? (userId as UsersId) : null,
})
).executeTakeFirstOrThrow();

// revalidateTagsForCommunity(["pubs"]);
} catch {
return { error: "The Pub was not successully assigned" };
}
Expand Down
Loading

0 comments on commit 76f44f2

Please sign in to comment.