RBAC permissions for creating relationships in GraphQL

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