RBAC permissions for creating relationships in GraphQL

I have a graphql mutation that partially updates one collection (organization) to create a new relation (invitation). I have been able to determine that I need ‘write’ AND ‘create’ permission in RBAC to create that new Invitation within Organization, but I am having trouble figuring out what the lambda arguments are to determine permissions. Basically, I want to check a property on the organization to determine whether the user is allowed to create the invitation. Could someone please help? I am starting to get comfortable with Fauna’s graphql and RBAC but this has me completely stumped. I should note that it works fine with Invitation’s write permission set to blanket ‘true’.

partialUpdateOrganization(id: $id, data: {
    invitations: {
        create: [
            { email: $email }
        ]
    }
}) {
    _id
}

Hi @rpagett!

If you use the dashboard to generate roles, it provides the following template for write privilege predicate function:

// Only write to your own data but
// don't change the owner of the data.
Lambda(
  ["oldData", "newData"],
  And(
    Equals(Identity(), Select(["data", "owner"], Var("oldData"))),
    Equals(
      Select(["data", "owner"], Var("oldData")),
      Select(["data", "owner"], Var("newData"))
    )
  )
)

oldData is the original data object before the write, and newData is what the data will look like after the write – i.e. already mutated with your input.

Thanks, @ptpaterson. An update to the issue: I was able to set up a write predicate, but create is an issue.

My theory: when a relationship is created through graphQL as in my mutation above, the ‘create’ predicate for the related object (invitations) does NOT receive a reference to the related top-level object (organization).

For example, my ‘UserInvitation’ type contains an email and a reference to the organization:

type UserInvitation {
  email: String!
  organization: Organization!
}

I create a reference in my mutation above. Ultimately, the built-in resolver adds the organization reference for me. But that organization reference does NOT appear in the data for the create predicate, so I can’t effectively check permissions. Part of this is conjecture, as I don’t know of a way to debug these predicate checks.

Could anyone confirm this suspicion for me?

Okay I think I understand. You want to make sure that a user can only creat new UserInvitation Documents for an organization they belong to, is that it?

Initial Notes & things to remember

Important for later: In One-to-one relationships, docs say “In the database, a single collection is predictably chosen to store relational data”. This predictable method is to use the collection with name that comes later in the alphabet. (There are some proposals to change that in the forums… but anyway). So, we should see the Organization references stored in the UserInvitation as well as User Documents.

Also, you can use Get in permission predicates! This means you can get User data and use that to match information in create and write ops.

Schema Definition

So if your schema is like this:

type Organization {
  name: String!
  invitations: [UserInvitation!]! @relation
  users: [User!]! @relation
}

type User {
  email: String
  name: String!

  organization: Organization! @relation # stored here!
}

type UserInvitation {
  email: String!
  organization: Organization! @relation # stored here!
}

Permission Required

I can confirm you need predicates for

  • write and create permission on UserInviation
  • write permission on Organization

to perform this mutation:

partialUpdateOrganization(id: $id, data: {
    invitations: {
        create: [
            { email: $email }
        ]
    }
})

So if you just turn all of those on it will work. But of course you don’t want them all true.

To be honest, I don’t know why you need create and write on the invitations. It must do the create, and then update to add the relation after the fact (an unfortunate doubling of write ops…). It also added to complexity.

Example Role

{
  ref: Role("user"),
  ts: 1593031454375000,
  name: "user",
  privileges: [
    {
      resource: Collection("Organization"),
      actions: {
        read: true,

        // Can only write to Organization if the data doesn't change.
        // lets the nested GraphQL query work
 
        write: Query(
          Lambda(
            ["oldData", "newData"],
            And(
              Equals(
                Select("data", Var("oldData")),
                Select("data", Var("newData"))
              )
            )
          )
        )
      }
    },
    {
      resource: Collection("UserInvitation"),
      actions: {
        read: true,

        // Allows any write if there is no Organization provided
        // or requires that Organization match the user's

        write: Query(
          Lambda(
            ["oldData", "newData"],
            Let(
              {
                inputOrg: Select(
                  ["data", "organization"],
                  Var("newData"),
                  null
                ),
                user: Get(Identity()),
                userOrg: Select(["data", "organization"], Var("user"), null)
              },
              If(
                IsNull(Var("inputOrg")),
                true,
                Equals(Var("inputOrg"), Var("userOrg"))
              )
            )
          )
        ),

        // Allows creation with any data if there is no Organization provided
        // or requires that Organization match the user's

        create: Query(
          Lambda(
            "values",
            Let(
              {
                inputOrg: Select(["data", "organization"], Var("values"), null),
                user: Get(Identity()),
                userOrg: Select(["data", "organization"], Var("user"), null)
              },
              If(
                IsNull(Var("inputOrg")),
                true,
                Equals(Var("inputOrg"), Var("userOrg"))
              )
            )
          )
        )
      }
    },
    {
      resource: Index("organization_invitations_by_organization"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    },
    {
      resource: Index("organization_users_by_organization"),
      actions: {
        unrestricted_read: false,
        read: true
      }
    }
  ],
  membership: [
    {
      resource: Collection("User")
    }
  ]
}

Alternative with UDF

If I got your use case right, and a User can only ever create invitations for their organization, then it might be better to only give the user call permissions for a UDF that takes limited input and creates an invitation by copying the user Organization into the invitation document. The UDF has all the permissions it needs (only create on UserInvitations).

This would be called from GraphQL by specifiying @resolver directive.

If you are interested in this idea I can elaborate.

1 Like

Thank you so much for your detailed answer. I did not consider using a UDF. I wasn’t quite sure how relations worked in FQL yet, but I can do some reading. I will probably go that route, and I’ll be sure to check back.

I do think it’s worth investigating whether the ‘organization’ reference should be available on the UserInvitation create predicate, both to avoid the double write op as you mentioned and to greatly simplify this situation.