Setting a role on the UDF vs. the User

Hey all,

I’m working on a multi-tennant app and I need to set up some roles to ensure a user can only see/edit the documents that they “own” in any given collection.

I’m planning on using UDFs to encapsulate all of my app logic, so that a user will just call the functions needed from the client - based on the Fwitter example. But, that example assumes that all users can see pretty much everything.

I’m struggling with where to assign the role I need. Can I assign it to the user document, and then assign no role to the UDF, and the UDF will inherit the user’s role? Or do I need to assign it to the UDF, and then assign I role to the User that just lets them call the UDF? If the latter, I presume something like Identity() will work on the user who’s calling the function, right?

Also, I’m unsure about how to set up the predicates on my role for Collections vs. Indexes. Assuming I’ve got a few Indexes that all operate on the same Collection, can I set up the role for read on the Collection, and the Indexes will respect that? Or do I need to set a predicate on all the Indexes as well?

Thanks!

That’s not entirely true of course. The app limits what you can see by using Identity() inside UDFs in combination with the encapsulation of the UDF. You could model your complete security model like that, that’s an option. It’s true that you can follow everyone and get all fweets via another way (tags), thats the nature of the app of course. An example of using roles for example to limit which user can write something (in this case, to change the password) was added on a later commit: https://github.com/fauna-brecht/fwitter/commit/9824df405d8655d06a28a975067bb042f2f5a0ba in a staging branch. It’s not merged yet since there is no article describing it yet. More examples that can be used for your inspiration are coming shortly via the excellent series from Pier: https://fauna.com/blog/getting-started-with-fql-faunadbs-native-query-language-part-1
part4 (or part5?) introduces roles with a lot of examples iirc.

When you assign the role to the user document and then create a token for it, the user will be able to do whatever your UDF is able to do. Which in some cases might defeat the purpose of using a UDF (in case you want to encapsulate some logic that you want to secure as one atomic unit) since a user can perfectly (if you call these from the frontend) go around the UDF and do a custom FQL query. If you are planning to call these from the backend, the only difference is that your backend key is able to do more than it would be able to if you only give the UDF access instead of the user but it comes down to the same thing.

I think it’s more clean personally (but more work) and the secure way in case you want to secure it from the frontend to create a role for your UDF and give your user access to call the UDF but as I said, if you call it from the backend, both options you present above would work.
Identity() will work on the user that is calling the function indeed. There is one caveat: Identity() will stop working in ‘membership’ of a role if you use functions, keep that in mind. We are working on making that more consistent in some way but that will not be available shortly.

Identity() can indeed be used to secure things. Since your user is bound to an identity via the token and your UDF does not take parameters that convey the identity, you could write your custom logic to access data or not straight in the UDF. Roles provide a cleaner separation of concerns (and can be used if you do not like to set up a UDF for everything).

Providing reads on a collection does not imply reads on an index, if you are using the index you have to give read access to both the index and the collection.

From another perspective, if you would just access the index (it has values which is essentially a kind of ‘view’) you would be able to without setting read access to the collection. However, the normal read permissions filters out tuples based on the source document visibility. There is an unfiltered_read permission (undocumented? Not sure) that allows you to give access to the index values without requiring access to the underlying source document (aka the collection). The idea is that in the normal case, if you can’t read a document you should not be able to indirectly read a part of it or ascertain existence of it via an index.

1 Like

Hi Brecht, thanks so much for taking the time to reply!

Sorry, I probably should have said something like “Fwitter is not a multi-tenant app” which I think better qualifies the distinction I’m trying to make. I want to restrict access to documents in a collection based on whether or not the user is in same tenant to which the document belongs.

That distinction between roles applied to UDFs vs. Users is really helpful. To make sure I’m understanding correctly, I can’t use Identity() when defining the memberships for a role that’s applied to a UDF, but it should work okay when defining the actions the role can perform on a Collection? In case it helps make it clearer, here’s a key snippet from my main “loggedInUser” role:

//... other role stuff
{
  resource: Collection("persons"),
  actions: {
    read: Query(
      Lambda(
        "ref",
        Equals(
          Select(
            ["data", "school"],
            Get(q.Match(q.Index("person_by_user"), Identity()))
          ),
          Select(["data", "school"], Get(Var("ref")))
        )
      )
    ),
    create: true,
  },
}

In my app, “Users” have a “Persons” ref, on which I store which “School” they belong to. I want a User only be able to read from the Persons collection if the person document they’re reading and the user share the same “school” ref.

Currently I’m applying this role to all members of the “users” collection via the memberships property of the role, but if I’m understanding you correctly, I could apply this role to my UDFs and get the same effect, and then create a role for my “users” that only granted them permissions to call UDFs? I can see where that would be preferable as it’d very explicitly limit what anyone could do from the client!

Thanks for the detail on Indexes and Collections as well - that’s helpful. For now I’m just adding restrictions at the Collection level and allowing reads from Indexes and that’s working great.

I’m really enjoying working with Fauna. It’s taken a bit to get productive with it but the Fwitter example was extremely helpful (especially for helping me figure out how to define my whole app DB infrastructure with code that I can deploy on command!) and the blog posts you referenced have finally helped me to get to a place where I feel much more comfortable with FQL. Can’t wait for parts 4 and 5! Anyway, just wanted to say thanks!

Just wanted to check in whether everything is working now? :slight_smile:

Thanks for checking in! I migrated things such that my users can now only call UDFs, and the UDFs have roles that grant access to collections/indexes, which I really like. I did still have some trouble with ABAC predicates for the UDF roles that I couldn’t quite figure out, but I worked around it by implementing some logic in the UDF itself to deal with multi-tennancy that I’m satisfied with for the time being.

I feel like it’d be super helpful to have a tool for testing role access in the console - Identity() doesn’t work in the shell so it’s pretty tough to figure out what’s broken. I’m not sure exactly what it’d look like, but I’d love to be able to test calling a UDF as a specific “user” so I can more easily validate role settings.

All that to say, I’m in a good spot now - thanks so much for the help!