Role for User vs. Role for UDF called by User

Hi, I’m trying to implement multi-tenancy in a single database using @databrecht 's suggestion:

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

I would like to use roles to restrict users to documents only within their own tenant. Every document in my database should have a “tenantRef” relationship to a Tenant collection. I have a User collection and role applied to them, and have have successfully been able to use predicate functions to programmatically restrict users to accessing other objects only in their tenant when they access them directly.

The problems start when I try to use UDFs. If I lock down the user to only be able to call UDFs and apply the exact same predicate to the role of a UDF the user can call, it always seems to fail with “permission denied” and “Insufficient privileges to perform the action.” This is the predicate I’m using via the dashboard, which works when applied to a User’s role, but not when applied to a UDF’s role:

Lambda(
  "documentRef",
  Equals(
    Select(["data", "tenantRef"], Get(CurrentIdentity())),
    Select(["data", "tenantRef"], Get(Var("documentRef")))
  )
)

Does CurrentIdentity() not do what I think it does in the context of a UDF? Is there something else I’m missing? Is there any way to inspect the value or execution of these predicates?

It seems to me like identity functions aren’t working correctly in the context of action predicates for UDFs. If I hard-code my User’s tenantRef, the following predicate function is working correctly to gate access:

Lambda(
  "documentRef",
  Equals(
    Ref(Collection("Tenants"), "298502253910688269"),
    Select(["data", "tenantRef"], Get(Var("documentRef")))
  )
)

However, the following two predicates which attempt to inspect the calling user do not work:

Lambda(
  "documentRef",
  IsRef(CurrentIdentity())
)

Lambda(
  "documentRef",
  HasCurrentIdentity()
)

And they both return:

  "errors": [
    {
      "cause": [
        {
          "code": "permission denied",
          "description": "Insufficient privileges to perform the action.",
          "position": [
            "expr",
            "in",
            "then"
          ]
        }
      ],
      "code": "call error",
      "description": "Calling the function resulted in an error.",
      "position": []
    }
  ]
}

I think that applying a Role to a UDF masks/overrides any previous Roles associated with the token/key. If that is true, there might not be any “Identity” to look for in the new context, and any permissions would have to be considered independent of the caller.

Some ways around this

The idea I am thinking is that if the User Role can limit call permissions for a UDF in certain circumstances, then the UDF Role can focus on what it needs to do, independent of the users’ identity.

The predicate for the call action receives the function arguments.

IMPORTANT! The call predicate MUST have the same number of arguments declared as the UDF, or it will always fail as permission denied. It took some experimentation to figure that out, but I am pretty sure.

Ignore the predicate arguments

UPDATE: I realize now you were not trying to use any meta-data in the UDF. So this won’t be applicable. Maybe this can just be food for thought – you can do pretty cool things when you treat fauna schema like any other Document!

If you store the tenent in the Function’s data, i.e.

// fauna shell
Update(
  Function('my_udf'), 
  { data: { tenantRef: Ref(Collection('User'), '263980061820977682') } }
)

then you can still Get it in the Predicate. It will cost an additional Read Op each time, though.

// udf
Lambda(["a", "b", "c"], /* ... */)
// role

// arg names don't have to match, but the arg count does!
// even if none of them are used
Lambda(["a", "b", "c"], 
  Equals(
    Select(["data", "tenantRef"], Get(Function('lambda'))), 

    // note this also assumes the user has permission to read 
    // it's own identity Document.
    Select(["data", "tenantRef"], Get(CurrentIdentity()) 
  )
)

Every UDF requires the tenant as an argument

this does not require any data saved in the UDF. But logic for the UDFs themselves may or may not be more complicated.

// udf
Lambda(["tenantRef", "a", "b", "c"], /* ... */)
// role
Lambda(["tenantRef", "a", "b", "c"], 
  Equals(
    Var("tenantRef"),
    Select(["data", "tenantRef"], Get(CurrentIdentity()) 
  )
)

Thanks for the response! This does give me some things to think about. I’ve been using something like your last solution to wrap every UDF (poor pseudo: If(Equals(objTenant, userTenant), /* real UDF code */, Abort('BAD_TENANT'))) which is working nicely. I was trying to simplify the complication it adds to every UDF by instead applying the check to the role the UDF runs as. I will probably just leave as-is since it does work.

I should have been a little more clear about my roles. My ultimate goal is to have one role called something like “loggedInUser” that is granted to my users who login, with privileges that look something like this:

privileges: [
        { resource: Function('TenantDeleteRoom'), actions: { call: true } },
        { resource: Function('TenantGetRooms'), actions: { call: true } },
        // only lots of functions
    ],

Then I would have a “tenantFunction” role that is assigned to each UDF (via role: Role("tenantFunction") during CreateFunction) and this role would limit access via something like:

  privileges: [
    {
      resource: Collection("Rooms"),
      actions: {
        // others too, this is just a sample
        delete: Query(
          Lambda(
            "documentRef",
            Equals(
              Select(["data", "tenantRef"], Get(CurrentIdentity())),
              Select(["data", "tenantRef"], Get(Var("documentRef")))
            )
          )
        )
      }
    },
    // lots of other collections too
  ]

This last case of predicate functions for a role assigned to a UDF is where I just can’t get the identity methods to work. They are working in the UDF itself, so it feels like I should just use them there instead.

1 Like

Oh! I definitely went and answered a question you didn’t ask didn’t I!

I think that applying a Role to a UDF masks/overrides any previous Roles associated with the token/key. If that is true, there might not be any “Identity” to look for in the new context, and any permissions would have to be considered independent of the caller.

I’ve updated my previous response to make it more like a related workaround.

1 Like

Thanks for the suggestions! I think keeping all the enforcement logic in the UDF will work for me for now, since I can at least compose the logic in my JavaScript migrations that create the UDF.

If identity doesn’t work for role predicates assigned to UDFs, it would be really good to document this. I spent many hours on something that I thought should work and Fauna highlights as a feature. It sounds like I’m not the first person to struggle with this either, if you read kgoggin’s last message in this thread.