Search code examples
keystonejs

KeystoneJS `filter` vs `Item` list access control


I am trying to understand more in depth the difference between filter and item access control.

Basically I understand that Item access control is, sort of, higher order check and will run before the GraphQL filter. My question is, if I am doing a filter on a specific field while updating, for instance a groupID or something like this, do I need to do the same check in Item Access Control? This will cause an extra database query that will be part of the filter.

Any thoughts on that?


Solution

  • The TL;DR answer...

    if I am doing a filter on a specific field [..] do I need to do the same check in Item Access Control?

    No, you only need to apply the restriction in one place or the other.

    Generally speaking, if you can describe the restriction using filter access control (ie. as a graphQL-style filter, with the args provided) then that's the best place to do it. But, if your access control needs to behave differently based on values in the current item or the specific changes being made, item access control may be required.

    Background

    Access control in Keystone can be a little hard to get your head around but it's actually very powerful and the design has good reasons behind it. Let me attempt to clarify:

    Filter access control is applied by adding conditions to the queries run against the database.

    Imagine a content system with lists for users and posts. Users can author a post but some posts are also editable by everyone. The Post list config might have something like this:

        // .. 
        access: {
            filter: {
                update: () => ({ isEditable: { equals: true } }),
            }
        },
        // ..
    

    What that's effectively doing is adding a condition to all update queries run for this list. So if you update a post like this:

    mutation {
        updatePost(where: { id: "123"}, data: { title: "Best Pizza" }) {
            id name
        }
    }
    

    The SQL that runs might look like this:

    update "Post"
    set title = 'Best Pizza'
    where id = 234 and "isEditable" = true;
    

    Note the isEditable condition that's automatically added by the update filter. This is pretty powerful in some ways but also has its limits – filter access control functions can only return GraphQL-style filters which prevents them from operating on things like virtual fields, which can't be filtered on (as they don't exist in the database). They also can't apply different filters depending on the item's current values or the specific updates being performed.

    Filter access control functions can access the current session, so can do things like this:

        filter: {
            // If the current user is an admin don't apply the usual filter for editability
            update: (session) => {
                return session.isAdmin ? {} : { isEditable: { equals: true } };
            },
        }
    

    But you couldn't do something like this, referencing the current item data:

        filter: {
            // ⚠️ this is broken; filter access control functions don't receive the current item ⚠️
            // The current user can update any post they authored, regardless of the isEditable flag
            update: (session, item) => {
                return item.author === session.itemId ? {} : { isEditable: { equals: true } };
            },
        }
    

    The benefit of filter access control is it doesn't force Keystone to read an item before an operation occurs; the filter is effectively added to the operation itself. This can makes them more efficient for the DB but does limit them somewhat. Note that things like hooks may also cause an item to be read before an operation is performed so this performance difference isn't always evident.

    Item access control is applied in the application layer, by evaluating the JS function supplied against the existing item and/or the new data supplied.

    This makes them a lot more powerful in some respects. You can, for example, implement the previous use case, where authors are allowed to update their own posts, like this:

        item: {
            // The current user can update any post they authored, regardless of the isEditable flag
            update: (session, item) => {
                return item.author === session.itemId || item.isEditable;
            },
        }
    

    Or add further restrictions based on the specific updates being made, by referencing the inputData argument.

    So item access control is arguably more powerful but they can have significant performance implications – not so much for mutations which are likely to be performed in small quantities, but definitely for read operations. In fact, Keystone won't let you define item access control for read operations. If you stop and think about this, you might see why – doing so would require reading all items in the list out of the DB and running the access control function against each one, every time a list was read. As such, the items accessible can only be restricted using filter access control.

    Tip: If you think you need item access control for reads, consider putting the relevant business logic in a resolveInput hook that flattens stores the relevant values as fields, then referencing those fields using filter access control.

    Hope that helps