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 Micropost
s only for themself.
User
s have friends. User
‘s have Micropost
s, 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
}
]
}
]