DEV Community

Maciej Krawczyk
Maciej Krawczyk

Posted on • Originally published at on

Building Your Own E-Commerce Keystone.js-Based System — Access Control

Building Your Own E-Commerce Keystone.js-Based System — Access Control

Improving upon our e-commerce based system

A grocery store
Photo by Nathália Rosa on Unsplash


It’s time to continue our series on creating a Keystone.js-based e-commerce system. Previously, we focused on creating all basic models and environment setups. This time we will work on access control and users privileges. Let’s be honest, not all users should be allowed to access all data and have the ability to modify it. It is a serious security risk, so this time we are going to fix it. Also, the finished code for this article is available on my GitHub.


So, what are our requirements here? First, we need to restrict users' access based on their role in the system. Basically, customers can only modify data associated with them, but admins have much more privileges. All of this should affect the system on three levels: first, restrict access to Admin UI, then filter access based on operation type to each schema, and lastly on the level of some fields in schemas.

Access Control Implementation

First, let’s update user roles in our system. In the previous article, we started with two of them — admin and customer. The basic assumption was that we need two different levels of security with user privileges, but after some consideration, I decided we need another one. So it’s time to update our roles.enum.ts and roles.const.ts and add the employee role. For now, privileges for admin and employee will stay the same, but with further system development it may change, as you can see below:

With that out of the way, we can focus on Admin UI access. For sure, all admins and employees should be able to use it; it’s necessary. But customers should not be able to access it. Of course, they need to log into the system and create sessions, but the Admin UI contains sensitive information that mustn’t be widely accessible.

Out of the box, Keystone offers the perfect solution for this kind of problem. To restrict certain users from accessing Admin UI, we just have to update our main config file, keystone.ts and ui.isAccessAllowed option. Its function with the context parameter will return a boolean value. Previously, we were only checking if users had valid session data. Now we have to check if the user has a valid session and if their role is not equal to customer.

And that’s all; only users with the right role will be able to access Admin UI. Now, it’s time to focus on access control at the level of each schema. Basically, there are two types of restrictions we are going to use: first, everyone can query but only internal users (admin and employee) can modify it. The second case covers a scenario when internal users have full access, but customer user can only query and update dates that belong to that user.

Let’s walk through all the schemas and update the access property on each of them. First, the Address list: it contains sensitive data so only admins and owners of specific records should be able to access it.

In Keystone.js’s access API, there are three levels of control (read more), operation, filter, and item. First one, operation allows restricting what kind of operation can be done on the list (query, create, update, and delete). Second, filter allows adding specific GraphQL filters to every operation performed on this list with distinction to its type (same as before, but without create). Last one, item works more on the data level and allows us to inspect data passed to each operation, but it works only on mutations, so has no effect on queries.

All properties in this section of the list configuration are functions. The first and last should return boolean; the second one should return GraphQL filter.

In case of Address list on the operation level, we only need to check if the user is authenticated, but on the filter level, we have to check if the user is admin or employee. Then there’s no necessity for an additional filter.

But when the user’s role is customer, we have to create and add filters limiting results only to this user. These functions will be rather similar for each schema, so I’ve decided to move them to another file to prevent unnecessary repetition. I’ve created a shared folder and index.ts inside. Also, I’ve created a file for our new method called filter-customer-access.ts and added it into exports in index.ts.

Basically, it creates and returns new GraphQL filter checking if the user ID in the record we are trying to access is equal to the ID stored in the current session. Additionally, I’ve added an interface to describe the session parameter, mostly because, in core Keystone files, a session has a type any that is not so convenient.

We are going to use this method to add this filter to query, update, and delete operations, but not create. First, it’s not possible, and it needs a different approach. We are going to uralitize item level restrictions in this case. The function used here will also be reused, so I’ve created another file in the shared folder called filter-customer-access-create.ts.

It looks pretty similar to the previous one, but there are two main differences: first, it returns a boolean value, not filter, and secondly, it checks if the data passed to create mutation has information about the user and if it’s the same user to whom current session belongs. Also, I’ve added export for this method in index.ts. Now it’s time to add it to the Address schema config file and its access section, as you can see below:

Order, Payment, Shipment has the exact same restrictions, so I’ve updated their schemas too. Additionally, in the Order schema, I’ve updated user and employee fields. Previously, they had an option to create a new entity from inside the list which is not really desired here. So I’ve added to this fields’ config:

ui: {
 hideCreate: true,
Enter fullscreen mode Exit fullscreen mode

Cart schema restrictions are pretty similar with some exceptions. First, there’s no check on the item level. Instead of that, I’ve added another config option — graphql. It contains options directly applied to GraphQL schema and allows disabling certain operations, create in our case.

Because of that, there’s no way to create new Cart entity via GraphQL API, and it’s something that we want. Cart for user can be created only once, on sign up process. More about that later. I’ve also updated some fields to block the possibility to create a new user and new products from inside this list. The last change was to add the default value to sum field. Here’s the whole updated schema:

The next group of lists has much simpler restrictions: all users can query them (not authenticated too), but only internal users can create/modify them. This group contains Catgegory, ProductImage, Product, and Stock. In order to apply these restrictions, we have to add access rules on the operation level:

In the Stock schema, I’ve also added an additional access rule on field level, amountInNextDelivery to be exact. Information about stock delivery may not be strictly confidential or sensitive, but competition is watching, so better keep it for authenticated users.

amountInNextDelivery: integer({
  access: { read: ({ session }) => !!session },
Enter fullscreen mode Exit fullscreen mode

The last updated schema is User; most changes are pretty similar to the Address list, but there are some exceptions. First, on the item level, we are not restricting the create operation. If we do, no one will be able to register.

Instead, I’ve added the same restriction to the update operation. Additionally, I’ve utilized hooks here, precisely the afterOperation one. We will talk more about hooks in further parts of this series, but for now, all we have to know is that they allow you to perform side effects in reference to operations performed on the current list.

Our hook first checks the type of current operation; if it was not the create operation, it returns undefined. But in the case of create, we want to proceed and create a corresponding Cart entity for that new user. But there’s a catch, previously when updating Cart schema I’ve disabled GraphQL API responsible for new cart creation, so we have to use prisma API (more about that). It allows us to skip this restriction without the risk of allowing users to create carts independently. Here’s the updated User schema:

There’s one thing left to do for this part. We need some test data — users in this case. Prisma.js included in Keystone has a nice way to import initial data (more), so let’s use this feature. Start with installing the ts-node package as devDependency:

yarn add -D ts-node
Enter fullscreen mode Exit fullscreen mode

In the meantime, create the folder, seed-data, and two files inside: data.json and index.ts. The first one will be our source of test data; the second will import that data. But additionally, we have to make small changes to our tsconfig.josn in order to be able to directly import data from the JSON file. So, under compiler options, add:

“resolveJsonModule”: true
Enter fullscreen mode Exit fullscreen mode

Now, let’s add our test data:

The password used here is 1234ABcd@@ for each user. Next, we have to prepare import logic, so let's update index.ts:

There are two main parts to this importer: first, we are creating a main method responsible for importing data into the database, and the second part that calls this function in a safe way with proper error catching and the ability to disconnect the database after the job is done.

The basic importer logic is pretty simple; it loads user data from imported JSON then loops over it. For each iteration of this loop, it checks if that user exists in the database; if not it creates it. With that done, the last thing we need here is a way to start this import. So, let’s update our package.json and add:

"prisma": {
  "seed": "ts-node seed-data"
Enter fullscreen mode Exit fullscreen mode

Additionally, we need to create a script to start all that using yarn, so under the scripts add:

"seed": "keystone prisma db seed"
Enter fullscreen mode Exit fullscreen mode

With that done, we are almost at the finish line of this part. We just need to create migrations for the updates we’ve done and import test data. So, let’s first start our database with the following command:

docker-compose -f up database
Enter fullscreen mode Exit fullscreen mode

Then, start our backend app outside the container. So in the backend folder run:

yarn dev
Enter fullscreen mode Exit fullscreen mode

The script will detect changes in the database schema and create a migration for us. We only have to specify a name for it. For example, new_user_role_and_cart_default_values. After that we can stop this script and seed our test data:

yarn seed
Enter fullscreen mode Exit fullscreen mode

With that done, we can restart the whole system and test it out with this command:

docker-compose -f up
Enter fullscreen mode Exit fullscreen mode


That’s all for this part. With that change, data security in our system improved much, and now we can safely proceed to the next tasks and work on the core elements of every e-commerce system — cart business logic.

I hope you liked it. If you have any questions or comments, feel free to ask them.

Have a nice day!

Top comments (0)