Read:predicate different behavior UDF vs. regular query

I have set up a read: predicate and I am seeing different behavior when I run in a UDF that has membership: versus running the query outright. I have read the docs, and do not understand the different behavior: User-defined roles - Fauna Documentation

First a minimal repro: GitHub - h-unterp/predicate_read
Follow the instructions in the readme, it should run with minimal setup.

First the role:

As you see, it has

membership: [{ resource: Collection(TestCollections.Users) }]

and a read predicate of

Equals(Select(["data", InfoModel.userRef], Get(Var("ref"))), CurrentIdentity())

Also, when run via UDF even

Equals(CurrentIdentity(), CurrentIdentity()),

evaluates to false (this is included in the comments of the file)

UDF Definition
The UDF LetItBe is defined here predicate_read/function.ts at main · h-unterp/predicate_read · GitHub

The Index, Collections, and data model
are defined here predicate_read/create.ts at main · h-unterp/predicate_read · GitHub

And two unit tests,

the first showing the non-UDF query working, and the second unit test showing the UDF query being denied permission.

If you follow the instructions in the readme, you will be able to reproduce this quite quickly.

  1. Wondering if this is a bug?
  2. Wondering if there is some way to both have my query in a UDF and have the read:predicate?
  3. If not, wondering about ideas of how to structure my system so it is both secure, and encapsulated in UDF’s?

But also, would really appreciate some perspective as to how to best achieve my goals.

Thank you.

I’ve done some more experimenting with unrestricted_read on a UDF and it too does not work.

What I don’t understand is that as the system is currently designed, we have to choose between encapsulating code as UDF which allows for some security, versus the ability to lock down things to a finer-grain with predicates however UDF’s don’t work in that case.

So either UDF’s or predicates, unless I’ve overlooked something?

Feels a bit inconsistent.

Being able to combine UDF’s with the predicate system would be extremely powerful, and allow for much more secure and consistently designed applications.

Would appreciate some perspective both about a potential feature request, but also how best to proceed designing the most secure app possible given current API. Thank you

This expression does not evaluate to false. It errors because there is no identity, so the predicate fails.

When you provide the role field to a UDF, the UDF runs as though it has its own Key with the Role provided, and Keys (as opposed to Tokens) do not have an Identity. Calling CurrentIdentity() with a Key returns an error, which you can test in the shell. When the predicate runs, it will encounter the same error, but the predicate fails as unauthorized rather than propagate the error to your users.

image

Since you are using the same Roles to call directly and assign to the functions, I will highlight that the membership field is only used to assign the Role to tokens as they are used; when you assign a Role to a Function, the membership is irrelevant (Has no effect on identity). But otherwise, there is no inherent problem with using a Role in both circumstances.

So is there a way to still use a UDF and restrict the reads? Yes!

We will need a different Role for the UDF than for a user calling the Index directly. I see that the UDF already has a "ref" parameter, but it is not used. What you need to do is require the caller to provide the user ref, and restrict the caller from only providing CurrentIdentity to the UDF.

CreateRole({
  name: "fn_role_let_it_be",
  // membership not required
  privileges: [
    // UDF is simply free to read the Collection and Index, because we are restricting how we call it
    {
      resource: Index(TestIndexes.InfoByUserRef),
      actions: {
        read: true,
      },
    },
    {
      resource: Collection(TestCollections.Info),
      actions: {
        read: true,
        write: true,
      },
    },
  ],
})
CreateFunction({
  name: TestFunctions.LetItBe,
  body: Query(Lambda("user_ref", 
    Get(Match(
      Index(TestIndexes.InfoByUserRef), 
      Var("user_ref")) // use the argument, don't call CurrentIdentity Directly
    ),
  )),
})
CreateRole({
  name: "user_can_call_UDFs",
  membership: [{ resource: Collection(TestCollections.Users) }],
  privileges: [
    {
      resource: Function(TestFunctions.LetItBe),
      actions: {
        call: Query(Lambda(["user_ref"],
          // User can ONLY call when providing their identity,
          Equals(Var("user_ref"), CurrentIdentity())
        )),
      },
    },
  ]
})

And now you should be able to use the UDF

// with a logged in user
client.query(
  Call(TestFunctions.LetItBe, CurrentIdentity())
)

side note

Your minimal repro is not very minimal. It was nontrivial to dig through the bespoke code you have and provide code that makes sense in response. The forums are a place for everyone to follow along with the discussion, and the way you’ve shared your code and schema makes that difficult.

I think that I have handle on what you are trying though, and I hope that my response helps. My advice (and request), though, is that for future questions you consider supplying the final definition of the schema involved rather than requiring readers to piece it together like this.

All that said, what you’ve done with CreateOrUpdateX helpers, breaking UDF bodies out into separate JS functions, etc. – I think that’s a great way to stay organized. Also it’s similar to things I’ve done before, which I think did help me get up to speed relatively quickly for your question :slight_smile:

2 Likes

Much appreciated. Re the minimal repro, noted, will provide schema if I need to ask another question. Thank you.

1 Like