Skip to content

e-hart/convex-rls-modify-aware

Repository files navigation

convex-rls-modify-aware

This is a modified version of the Row Level Security (RLS) helpers from convex-helpers. It simply passes the incoming update values to the Rules' modify function.

Why?

The original helpers are not aware of the update value when checking for write access.

Usage

Copy the ./convex/rowLevelSecurity/ directory and its contents to your project.

// ./convex/rls.ts

import {
  Rules,
  wrapDatabaseReader,
  wrapDatabaseWriter,
} from "./rowLevelSecurity";

import {
  customCtx,
  customMutation,
  customQuery,
} from "convex-helpers/server/customFunctions";

import { DataModel } from "./_generated/dataModel";
import { mutation, query, QueryCtx } from "./_generated/server";

async function rlsRules(ctx: QueryCtx) {
  const identity = await ctx.auth.getUserIdentity();
  return {
    users: {
      read: async (ctx, user) => ...
      insert: async (ctx, user) => ...
      modify: async (ctx, user, updateValues) => ...
    },
    groups: { 
      read: async (ctx, groupBeingRead) => ...
      insert: async (ctx, groupToInsert) => ...
      modify: async (ctx, currentGroupDoc, updateValues) => {
            
        // Example 1: Only allow updates to specific fields
        const allowedFields = ['name', 'description', 'isPublic'];
        const updateKeys = Object.keys(updateValues);
        const hasUnauthorizedFields = updateKeys.some(key => !allowedFields.includes(key));
        if (hasUnauthorizedFields) return false;

        // Example 2: Prevent changing group ownership
        if ('ownerId' in updateValues && updateValues.ownerId !== doc.ownerId) {
            return false;
        }

        // Example 3: Only group members can update certain fields
        if ('memberIds' in updateValues && !doc.memberIds.includes(ctx.userId)) {
            return false;
        }

        // Example 4: Validate field values before allowing update
        if ('isPublic' in updateValues && updateValues.isPublic === true) {
            // Only admins can make groups public
            if (!isAdmin(ctx)) return false;
        }

        // Example 5: Prevent removing all members from a group
        if ('memberIds' in updateValues && Array.isArray(updateValues.memberIds)) {
            if (updateValues.memberIds.length === 0) return false;
        }

        // Example 6: Rate limiting updates to group settings
        if ('settings' in updateValues) {
            const lastUpdate = doc.lastSettingsUpdate || 0;
            const now = Date.now();
            if (now - lastUpdate < 60000) { // 1 minute cooldown
                return false;
            }
        }

        return false;
      },
    },
  } satisfies Rules<QueryCtx, DataModel>;
}

export const queryWithRLS = customQuery(
  query,
  customCtx(async (ctx) => ({
    db: wrapDatabaseReader(ctx, ctx.db, await rlsRules(ctx)),
  })),
);

export const mutationWithRLS = customMutation(
  mutation,
  customCtx(async (ctx) => ({
    db: wrapDatabaseWriter(ctx, ctx.db, await rlsRules(ctx)),
  })),
);

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published