Reindex
TeamBlogDocs

Authorization

An important part of the security of your application is authorization. You need to control what data which users can access in your app.

Role based access control (RBAC) is the de facto standard for authorization. However, it introduces many complexities for practical applications: roles can be inflexible to change, and additional mapping between users and roles must be maintained separately.

Ideally we should be able to derive the authorizations from the application data using access rules. Such rules could be written with code, but maintaining the code can become difficult when the data model evolves.

Reindex permissions are declarative rules, defined in the schema of the application and make use of the data graph of the application the determine access at runtime.

Default permissions

Requests made with an admin token always have all the permissions to all types. Otherwise, the permissions list in your type in the schema determines the permissions.

If a type doesn’t have permissions defined in the schema, then by default everyone gets full permissions to do anything to the nodes of that type.

Built-in types require admin permissions to work with.

Permissions in the schema

The permissions property of a type defines the permissions for that type. For example, you could give all logged-in users a permission to read comments in your app by adding this permission list to a Comment type in the schema:

permissions: [
  {
    grantee: 'AUTHENTICATED',
    read: true,
  },
]

The full type definition for elements of the permissions list is:

type ReindexPermisssion {
  grantee: ReindexGrantee!
  userPath: [String]
  read: Boolean
  create: Boolean
  update: Boolean
  delete: Boolean
  permittedFields: [String]
}

enum ReindexGrantee {
  EVERYONE
  AUTHENTICATED
  USER
}

grantee determines the scope of the permission. Permission can be granted either to:

  • EVERYONE (all users, including anonymous)
  • AUTHENTICATED logged-in users) or
  • USER, a User related to the node (see User permissions below). When grantee is USER, userPath defines the path to the related user from the node in question.

read, create, update and delete determine if a particular permission is granted to the grantee.

  • Any operations that read nodes require a read permission. Read permission is also checked when listing the nodes in a connection field.

  • Creating nodes requires a create permission and field permissions on all fields that node is created with.

  • Updating nodes requires an update permission and permissions for each field that is being updated. Replacing nodes also requires an update permission and permissions for fields both in old and new nodes.

  • Deleting a node requires a delete permission. Adding or removing items from a one-to-many or many-to-many relationship requires an update permission to the connection fields for both nodes.

permittedFields allows you to specify the fields a create, update or delete permission applies to. If permittedFields is omitted, the permissions are granted on all fields.

User permissions

In addition to granting permissions to all or logged-in users, it’s possible to grant permissions to users that are related to the node in question somehow. The relationship is specified with userPath, which should be a list of fields that will form a chain that terminates at a reference to a User node or a connection to User nodes. Each element of a chain should be a name of a node reference or a connection.

For example, we could allow only the author of a comment to delete it:

{
  grantee: 'USER',
  userPath: ['author'],
  delete: true,
}

Additionally, we can let only the friends of the author read comments:

{
  grantee: 'USER',
  userPath: ['author', 'friends'],
  read: true,
}

Permissions to own User node

Permissions to User own node is a special case of User permissions. They are granted by having a permission in type User with userPath set to ["id"].

For example, to let users update their own user profile, you can this permission to the User type:

{
  grantee: 'USER',
  userPath: ['id'],
  update: true,
}

Examples

Let’s consider another example - there are users and each User can be in a team. A user can have another user as supervisor. Each User can read and update themself, team members can read each other. Supervisor can be read by their subordinates and can read and update them. Members of the team can read the team.

[
  {
    kind: "OBJECT",
    interfaces: [
      "Node"
    ],
    name: "User",
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "team",
        type: "Team",
        reverseName: "members"
      },
      {
        name: "supervisor",
        type: "User",
        reverseName: "supervises"
      },
      {
        name: "supervises",
        type: "Connection",
        ofType: "User",
        reverseName: "supervisor"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "id"
        ],
        read: true,
        update: true
      },
      {
        grantee: "USER",
        userPath: [
          "supervisor"
        ],
        read: true,
        update: true
      },
      {
        grantee: "USER",
        userPath: [
          "supervises"
        ],
        read: true
      },
      {
        grantee: "USER",
        userPath: [
          "team",
          "members"
        ]
      }
    ]
  },
  {
    kind: "OBJECT",
    interfaces: [
      "Node"
    ],
    name: "Team",
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "members",
        type: "Connection",
        ofType: "User",
        reverseName: "team"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "members"
        ],
        read: true
      }
    ]
  }
]

When nodes have relationships to other nodes, permissions make sure that the user has permission to modify the other side of the relationship too, before allowing a mutation. Let’s consider the schema below.

[
  {
    kind: "OBJECT",
    name: "User",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "microposts",
        type: "Connection",
        ofType: "Micropost",
        reverseName: "author"
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "Micropost",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "microposts"
      }
    ]
  }
]

Micropost nodes can have a reference to User nodes. User nodes have a connection to all Micropost nodes that reference them. To do a mutation that will change the contents of the User.microposts connection, you need to have a permission to update it.

For example, when creating a Micropost, a permission to User.microposts is also required, as this field is referenced by the author field. When updating or replacing a node reference, an update permission to corresponding connection field is required, in both the existing and the new node referenced. For deleting, an update permission to the connection field in the currently referenced node is required.

Additionally, deleting will be blocked, if the connection fields of the node still contain any nodes. E.g. you can’t delete a User node that has anything in microposts.

One important use case for permittedFields is granting of permissions only on the connection fields of types, thus allowing other nodes to connect, but not allowing any other operations. For example, logged-in user can have a permission to add Microposts only for themself.

Example

Users have friends. User‘s have Microposts, Micropost‘s have comments. Friends can read and add comments to their friends’ microposts. Users can only create microposts where they are the author. Friends can read each other. Users can delete any comments to their microposts. Friends can read all comments to friends’ microposts. Authors of comments can update and delete their comments.

[
  {
    kind: "OBJECT",
    name: "Micropost",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "text",
        type: "String",
        orderable: true
      },
      {
        name: "createdAt",
        type: "DateTime",
        orderable: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "microposts"
      },
      {
        name: "comments",
        type: "Connection",
        ofType: "Comment",
        reverseName: "micropost"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "author"
        ],
        create: true,
        read: true,
        update: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "author",
          "friends"
        ],
        read: true,
        permittedFields: [
          "comments"
        ]
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "User",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "handle",
        type: "String",
        unique: true
      },
      {
        name: "email",
        type: "String"
      },
      {
        name: "friends",
        type: "Connection",
        ofType: "User",
        reverseName: "friends"
      },
      {
        name: "microposts",
        type: "Connection",
        ofType: "Micropost",
        reverseName: "author",
        defaultOrdering: {
          field: "createdAt",
          order: "ASC"
        }
      },
      {
        name: "comments",
        type: "Connection",
        ofType: "Comment",
        reverseName: "author"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "id"
        ],
        read: true,
        update: true,
        permittedFields: [
          "handle",
          "email",
          "friends",
          "microposts",
          "comments"
        ]
      },
      {
        grantee: "USER",
        userPath: [
          "friends"
        ],
        read: true
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "Comment",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "text",
        type: "String",
        orderable: true
      },
      {
        name: "createdAt",
        type: "DateTime",
        orderable: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "comments"
      },
      {
        name: "micropost",
        type: "Micropost",
        reverseName: "comments"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "author"
        ],
        create: true,
        read: true,
        update: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "micropost",
          "author"
        ],
        read: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "micropost",
          "author",
          "friends"
        ],
        read: true
      }
    ]
  }
]
Topics:

Authorization

An important part of the security of your application is authorization. You need to control what data which users can access in your app.

Role based access control (RBAC) is the de facto standard for authorization. However, it introduces many complexities for practical applications: roles can be inflexible to change, and additional mapping between users and roles must be maintained separately.

Ideally we should be able to derive the authorizations from the application data using access rules. Such rules could be written with code, but maintaining the code can become difficult when the data model evolves.

Reindex permissions are declarative rules, defined in the schema of the application and make use of the data graph of the application the determine access at runtime.

Default permissions

Requests made with an admin token always have all the permissions to all types. Otherwise, the permissions list in your type in the schema determines the permissions.

If a type doesn’t have permissions defined in the schema, then by default everyone gets full permissions to do anything to the nodes of that type.

Built-in types require admin permissions to work with.

Permissions in the schema

The permissions property of a type defines the permissions for that type. For example, you could give all logged-in users a permission to read comments in your app by adding this permission list to a Comment type in the schema:

permissions: [
  {
    grantee: 'AUTHENTICATED',
    read: true,
  },
]

The full type definition for elements of the permissions list is:

type ReindexPermisssion {
  grantee: ReindexGrantee!
  userPath: [String]
  read: Boolean
  create: Boolean
  update: Boolean
  delete: Boolean
  permittedFields: [String]
}

enum ReindexGrantee {
  EVERYONE
  AUTHENTICATED
  USER
}

grantee determines the scope of the permission. Permission can be granted either to:

  • EVERYONE (all users, including anonymous)
  • AUTHENTICATED logged-in users) or
  • USER, a User related to the node (see User permissions below). When grantee is USER, userPath defines the path to the related user from the node in question.

read, create, update and delete determine if a particular permission is granted to the grantee.

  • Any operations that read nodes require a read permission. Read permission is also checked when listing the nodes in a connection field.

  • Creating nodes requires a create permission and field permissions on all fields that node is created with.

  • Updating nodes requires an update permission and permissions for each field that is being updated. Replacing nodes also requires an update permission and permissions for fields both in old and new nodes.

  • Deleting a node requires a delete permission. Adding or removing items from a one-to-many or many-to-many relationship requires an update permission to the connection fields for both nodes.

permittedFields allows you to specify the fields a create, update or delete permission applies to. If permittedFields is omitted, the permissions are granted on all fields.

User permissions

In addition to granting permissions to all or logged-in users, it’s possible to grant permissions to users that are related to the node in question somehow. The relationship is specified with userPath, which should be a list of fields that will form a chain that terminates at a reference to a User node or a connection to User nodes. Each element of a chain should be a name of a node reference or a connection.

For example, we could allow only the author of a comment to delete it:

{
  grantee: 'USER',
  userPath: ['author'],
  delete: true,
}

Additionally, we can let only the friends of the author read comments:

{
  grantee: 'USER',
  userPath: ['author', 'friends'],
  read: true,
}

Permissions to own User node

Permissions to User own node is a special case of User permissions. They are granted by having a permission in type User with userPath set to ["id"].

For example, to let users update their own user profile, you can this permission to the User type:

{
  grantee: 'USER',
  userPath: ['id'],
  update: true,
}

Examples

Let’s consider another example - there are users and each User can be in a team. A user can have another user as supervisor. Each User can read and update themself, team members can read each other. Supervisor can be read by their subordinates and can read and update them. Members of the team can read the team.

[
  {
    kind: "OBJECT",
    interfaces: [
      "Node"
    ],
    name: "User",
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "team",
        type: "Team",
        reverseName: "members"
      },
      {
        name: "supervisor",
        type: "User",
        reverseName: "supervises"
      },
      {
        name: "supervises",
        type: "Connection",
        ofType: "User",
        reverseName: "supervisor"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "id"
        ],
        read: true,
        update: true
      },
      {
        grantee: "USER",
        userPath: [
          "supervisor"
        ],
        read: true,
        update: true
      },
      {
        grantee: "USER",
        userPath: [
          "supervises"
        ],
        read: true
      },
      {
        grantee: "USER",
        userPath: [
          "team",
          "members"
        ]
      }
    ]
  },
  {
    kind: "OBJECT",
    interfaces: [
      "Node"
    ],
    name: "Team",
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "members",
        type: "Connection",
        ofType: "User",
        reverseName: "team"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "members"
        ],
        read: true
      }
    ]
  }
]

When nodes have relationships to other nodes, permissions make sure that the user has permission to modify the other side of the relationship too, before allowing a mutation. Let’s consider the schema below.

[
  {
    kind: "OBJECT",
    name: "User",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "microposts",
        type: "Connection",
        ofType: "Micropost",
        reverseName: "author"
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "Micropost",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "microposts"
      }
    ]
  }
]

Micropost nodes can have a reference to User nodes. User nodes have a connection to all Micropost nodes that reference them. To do a mutation that will change the contents of the User.microposts connection, you need to have a permission to update it.

For example, when creating a Micropost, a permission to User.microposts is also required, as this field is referenced by the author field. When updating or replacing a node reference, an update permission to corresponding connection field is required, in both the existing and the new node referenced. For deleting, an update permission to the connection field in the currently referenced node is required.

Additionally, deleting will be blocked, if the connection fields of the node still contain any nodes. E.g. you can’t delete a User node that has anything in microposts.

One important use case for permittedFields is granting of permissions only on the connection fields of types, thus allowing other nodes to connect, but not allowing any other operations. For example, logged-in user can have a permission to add Microposts only for themself.

Example

Users have friends. User‘s have Microposts, Micropost‘s have comments. Friends can read and add comments to their friends’ microposts. Users can only create microposts where they are the author. Friends can read each other. Users can delete any comments to their microposts. Friends can read all comments to friends’ microposts. Authors of comments can update and delete their comments.

[
  {
    kind: "OBJECT",
    name: "Micropost",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "text",
        type: "String",
        orderable: true
      },
      {
        name: "createdAt",
        type: "DateTime",
        orderable: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "microposts"
      },
      {
        name: "comments",
        type: "Connection",
        ofType: "Comment",
        reverseName: "micropost"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "author"
        ],
        create: true,
        read: true,
        update: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "author",
          "friends"
        ],
        read: true,
        permittedFields: [
          "comments"
        ]
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "User",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "handle",
        type: "String",
        unique: true
      },
      {
        name: "email",
        type: "String"
      },
      {
        name: "friends",
        type: "Connection",
        ofType: "User",
        reverseName: "friends"
      },
      {
        name: "microposts",
        type: "Connection",
        ofType: "Micropost",
        reverseName: "author",
        defaultOrdering: {
          field: "createdAt",
          order: "ASC"
        }
      },
      {
        name: "comments",
        type: "Connection",
        ofType: "Comment",
        reverseName: "author"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "id"
        ],
        read: true,
        update: true,
        permittedFields: [
          "handle",
          "email",
          "friends",
          "microposts",
          "comments"
        ]
      },
      {
        grantee: "USER",
        userPath: [
          "friends"
        ],
        read: true
      }
    ]
  },
  {
    kind: "OBJECT",
    name: "Comment",
    interfaces: [
      "Node"
    ],
    fields: [
      {
        name: "id",
        type: "ID",
        nonNull: true,
        unique: true
      },
      {
        name: "text",
        type: "String",
        orderable: true
      },
      {
        name: "createdAt",
        type: "DateTime",
        orderable: true
      },
      {
        name: "author",
        type: "User",
        reverseName: "comments"
      },
      {
        name: "micropost",
        type: "Micropost",
        reverseName: "comments"
      }
    ],
    permissions: [
      {
        grantee: "USER",
        userPath: [
          "author"
        ],
        create: true,
        read: true,
        update: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "micropost",
          "author"
        ],
        read: true,
        delete: true
      },
      {
        grantee: "USER",
        userPath: [
          "micropost",
          "author",
          "friends"
        ],
        read: true
      }
    ]
  }
]