Allowlist in predicate function

In the docs I’ve seen that you can deny writing to properties like so:

client.query(q.CreateRole({
  name: "users",
  membership: [
    {
      resource: q.Collection("User"),
    },
  ],
  privileges: [
    {
      resource: q.Collection("User"),
      actions: {
        read: q.Query(q.Lambda("ref", q.Equals(q.Identity(), q.Var("ref")))),

        // TODO: Convert from denylist into allowlist
        write: q.Query(
          q.Lambda(
            ["oldData", "newData"],
            q.And(
              q.Equals(
                q.Select(["data", "email"], q.Var("oldData")),
                q.Select(["data", "email"], q.Var("newData")),
              ),
              q.Equals(
                q.Select(["data", "role"], q.Var("oldData")),
                q.Select(["data", "role"], q.Var("newData")),
              ),
              q.Equals(
                q.Select(["data", "customerID"], q.Var("oldData")),
                q.Select(["data", "customerID"], q.Var("newData")),
              ),
            ),
          ),
        ),
        
        delete: q.Query(q.Lambda("ref", q.Equals(q.Identity(), q.Var("ref")))),
      },
    }
  ],
});

As I add new props to a user document I need to then potentially also remember to block writing to that new prop. I’d prefer to instead create an allowlist to be more explicit. I’ve tried the following:

write: q.Query(
  q.Lambda(
    ["oldData", "newData"],
    q.And(
    
      // Only allow the user that owns the document to update it
      q.Equals(q.Identity(), q.Select(["ref"], q.Var("oldData"))),
    
      // Allowlist
      q.IsEmpty(
        q.Difference(
          q.Map(
            q.ToArray(q.Var("newData")),
            q.Lambda(["k", "v"], q.Var("k")),
          ),
          ["shoppingCart"], // This is the prop that I want to allow the user to update
        ),
      ),
    ),
  ),
),

I’ve spent half the day going through different variations of this and can’t get it to work. It’s super difficult to debug predicate functions and I’m kinda just stabbing in the dark here…

Ideally I would also like to set in the allowlist what data type the properties should be. Eg, in the FQL above I’d like to allow the user to only update the shoppingCart property which should be of type Ref.

The GraphQL endpoint enforces the data types in the mutation input types but a crafty user could still send HTTP queries with the wrong data type.

Additionally, I’m also curious about the following:

  1. Is there any way to debug predicate functions, or at least get some insight into their internals?
  2. What is the exact shape of the write lambda inputs? It seems I can’t access the ref prop.
  3. Is it possible for a user to change props like ref, _ts, etc?

How about this?

write: q.Query(
  q.Lambda(
    ["oldData", "newData"],
    q.Let(
      {
        whitelist: {
          shoppingCart: true
        }
      }
      q.And(
      
        // Only allow the user that owns the document to update it
        q.Equals(q.Identity(), q.Select(["ref"], q.Var("oldData"))),
      
        // Allowlist
        q.Equals(
          q.Merge(q.Var("oldData"), q.Var("whitelist")),
          q.Merge(q.Var("newData"), q.Var("whitelist")),
        ),
      )
    )
  ),
)
1 Like

That’s a clever approach @ptpaterson! Thanks for that! :pray:

@ptpaterson How deep does the equality check go? Will it work for:

{
  data: {
    attrInWhitelist: 'abc',
    valid: {
      to: some-time, 
      from: another-time
    }
  }
}

E.g. if you change both attrInWhitelist and valid.from, but valid.from isn’t whitelisted?

The “white-list”, as I offered to use it, would just need to be defined properly.

I’m suggesting to use Merge to modify and compare oldData and newData. So you can check out the docs on Merge, experiment, and finds what works for you.

Big note on Merge is that it is a “deep” operation. For example:

Merge(
  {
    attrInWhitelist: 'abc',
    valid: {
      to: some-time, 
      from: another-time
    }
  },
  {
    valid: {
      to: LATEST_TIME
    }
  },
)

// returns
{
  attrInWhitelist: 'abc',
  valid: {
    to: LATEST_TIME, 
    from: another-time
  }
}

Note how valid.from is unchanged.

That means that the following white-list (without testing, I’ve not tested any of this) should work to allow changes for attrInWhitelist and the valid.to field, but NOT the valid.from field. If you just want to not allow any changes to the valid property, then don’t include it in the white list at all. And if the latter was all you were trying to ask, then sorry it took so long for me to get there! :nerd_face:

/*...*/
whitelist: {
  attrInWhitelist: true,
  valid: {
    to: true
  }
}
/*...*/