Search code examples
javascripttypescriptgraphqltypegraphql

Type-GraphQL: Allow document owner and admin/moderator access with the @Authorized() decorator


Details

I am using type-graphql and are trying to restrict data access or actions for a specific group of users with the @Authorized() decorator and a custom auth checker as a guard. Everything is working and access is blocked/allowed appropriately according to the decorator.

Problem

I want the owner/author of the document to be able to edit/delete it even if its marked with @Authorized(["MODERATOR", "ADMIN"]) optionally I could mark it with something like @Authorized(["OWNER", "MODERATOR", "ADMIN"]) if that makes it easier to implement.

As far as I know I dont have any information as to what Model/document the user is trying to access in the auth checker only the arguments of the mutation. In other words I have the ID of what they want to access but, not the Model it belongs to.

Question

Is there any way I can check if the user owns the document in the auth checker or will I have to mark it as @Authorized() and check if the user owns the document or is an admin/moderator in every mutation/query?

Code

index.d.ts

declare module "type-graphql" {
    function Authorized(): MethodAndPropDecorator;
    function Authorized(roles: AccessLevels[]): MethodAndPropDecorator;
    function Authorized(...roles: AccessLevels[]): MethodAndPropDecorator;
}

types.ts

type AccessLevels = "MODERATOR" | "ADMIN";

authChecker.ts

const authChecker: AuthChecker<{ user: User | null }, AccessLevels> = ({ context }, roles): boolean => {
    if (!context.user) {
        return false;
    }

    if (roles.length === 0) {
        return true;
    }

    if (context.user.accessLevel > 0 && roles.includes("MODERATOR")) {
        return true;
    }

    return context.user.accessLevel > 1 && roles.includes("ADMIN");
};

EstablishmentResolver.ts

@Authorized(["MODERATOR", "ADMIN"])
@Mutation(() => EstablishmentType, { description: "Deletes a establishment by ID" })
async deleteEstablishment(@Args() { id }: EstablishmentIdArg): Promise<Establishment> {
    const establishment = await Establishment.findOne({ where: { id } });

    if (!establishment) {
        throw new Error("Establishment does not exist");
    }

    await establishment.destroy();

    return establishment;
}

Solution

  • Try to modify the signature and logic to allow passing a callback into the decorator, that will resolve the "owner" condition:

    @ObjectType
    class User {
      @Authorized({ 
        roles: ["MODERATOR", "ADMIN"],
        owner: ({ root, context }) => {
          return root.id === context.user.id;
        },
      })
      @Field()
      email: string;
    }
    

    Be aware that you can't always do that inside the authChecker or middleware as they run before your resolver code, so in case of deleteEstablishment there's no universal way to match establishmentId with user.id to detect if it's an owner or not.