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.
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.
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) orUSER, 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.
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,
}
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,
}
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.
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
}
]
}
]