crystal icon indicating copy to clipboard operation
crystal copied to clipboard

A solution to add authorization on root fields via the makeWrapPlansPlugin in V5

Open maikdiepenbroek opened this issue 4 months ago • 0 comments

After considering multiple solutions (that were known to me) including a schema-first approach with directives I've started discussing with @benjie where he pointed me to some great alternatives for achieving authorization on root fields.

His proposal:

makeWrapPlansPlugin({
  Mutation: {
    updateCustomer(plan, _, { $customerId }) {
      const $hasWriteAccess = currentUserHasWriteAccessToCustomerId($customerId);
      sideEffect($hasWriteAccess, (hwa) => {
        if (!hwa) throw new Error("No write access!")
      });
      return plan();
    }
  }
})

With currentUserHasWriteAccessToCustomerId being:

function currentUserHasWriteAccessToCustomerId($customerId) {
  const $currentUserId = context().get('currentUserId');
  const $access = customerPrivileges.find();
  $access.where(sql`${$access}.user_id = ${$access.placeholder($currentUserId, TYPES.int)}`);
  const $customerIds = applyTransforms(each($access, $row => $row.get('customer_id')));
  return lambda([$customerIds, $customerId], ([customerIds, customerId]) => customerIds.includes(customerId));

I settled on an implementation that worked for my use case, which is as follows:

import { context, lambda, sideEffect } from "postgraphile/grafast";
import { makeWrapPlansPlugin } from "postgraphile/utils";
import { GraphQLError } from "postgraphile/graphql";

const USER_ROLE_ADMIN = "ADMIN";

const getUserRoleFromContext = () => {
  return context().get("pgSettings").get("myapp.user.userRole");
};

const userHasAdminRole = (userRole: string) => {
  if (userRole !== USER_ROLE_ADMIN) {
    throw new GraphQLError(
      "User does not have the correct role to access this resource.",
    );
  }
};

// I've added :any for the plans here, since the proper type: SmartFieldPlanResolver is not exported and TS doesn't infer it correctly.
export const AuthorizationPlugin = makeWrapPlansPlugin({
  Mutation: {
    createCustomer(plan: any) {
      sideEffect(getUserRoleFromContext(), userHasAdminRole);
      return plan();
    },
    editCustomerById(plan: any) {
      sideEffect(getUserRoleFromContext(), userHasAdminRole);
      return plan();
    },
  },
});

maikdiepenbroek avatar Oct 07 '24 17:10 maikdiepenbroek