Skip to main content

Introduction

What you'll learn
  • what are security permissions and how to use them in code
  • how to add custom permissions UI to permissions editor in Admin Area
info

Learn more about the Webiny Security Framework in the Security Framework key topics section.

Overview#

In previous two sections, we've seen how we can quickly expand the GraphQL schema of the main GraphQL API, and also how we can add new modules in the Webiny Admin Area application. All with a couple of built-in scaffolding utilities.

In this tutorial, we expand our application by adding a layer of security around it. With it, we want to achieve the following:

  1. prevent users from executing GraphQL API queries and mutations, if they don't have the necessary security permissions
  2. prevent users from accessing modules we've added into the Webiny Admin Area application, if the user doesn't have the necessary security permissions

Note that in order to implement both behaviours, we are relying on security permissions, which is the first step of implementing a security layer for our application, and is the first topic we cover in this introduction section.

info

If you want to learn more about the main GraphQL API and how it works on the cloud infrastructure level, check out the GraphQL Requests page of the Cloud Infrastructure - API key topics section.

info

The code that we will cover in this tutorial can also be found in our GitHub examples repository.

Security Permissions#

It all starts with security permissions.

On the code level, security permissions are just plain JavaScript objects, which allow us to perform identity authorization. In other words, these objects contain information that tells us which resources an identity (for example, a logged-in user) can access, and which it cannot.

"Can this user access this GraphQL query?", "Can this user edit this particular piece of data?", or "Can this user view this particular piece of user interface?", are just some questions that security permission objects can answer for us.

Except for the required name property, these objects don't follow any strict structure, which means that while we're developing our custom application, we can structure them in any way that fits our needs and business logic.

A simple security permission object might look something like the following:

{
// "name" property is required, the rest are optional.
name: "my.custom.permission.object",
canRead: true,
canWrite: true,
hasAccessToSomethingGreat: "maybe",
canAccessUntil: "2025-06-01",
somethingNested: {
someRule: "here"
}
}

Assigning Security Permissions to Identities#

Within Webiny Admin Area application's security model, security permissions are assigned to identities via security groups.

Every identity (for example, a logged in user) belongs to exactly one security group. But, note that a single security group can have multiple security permissions objects assigned to it, allowing or preventing access across multiple Webiny and your custom applications.

This can be seen while creating a new security group via the security groups module.

Security Groups Module

By default, we get to specify permissions for every installed Webiny application:

Only a Single Security Group Allowed

Making changes in one or more of these sections will, internally, result in one or more permission objects to be created. And, once we submit the form, all of the different permission objects that were created in the process, are sent to the GraphQL API and stored in the database (along with other general security group information - name, slug, and description).

Ultimately, no matter if we're performing authorization on the API side or in the UI, via a couple of built-in code utilities, we can retrieve identity's security group and all of the permissions it contains. With that data, we can then determine whether a user has access to a particular API resource or a section in the UI (more on this in the following sections).

Creating Custom Security Permissions#

By adding a new permissions section in the security group form that we've seen above, we can allow system administrators to manually toggle access for our own custom features, for all identities that are linked with the particular security group.

Continuing with the car manufacturers example that we've used in previous tutorials, we could add a new Car Manufacturers section, and offer a couple of different options to the system administrator:

Added Security Permissions Group

To achieve this, within the Webiny Admin Area React application, we need to register the following admin-app-permissions-renderer plugin:

packages/car-manufacturers/admin-app/src/permissions/index.tsx
import React, { useCallback, useMemo } from "react";
import { i18n } from "@webiny/app/i18n";
import { AdminAppPermissionRendererPlugin } from "@webiny/app-admin/types";
// UI React Components - let's make it look nice:
import { Grid, Cell } from "@webiny/ui/Grid";
import { AccordionItem } from "@webiny/ui/Accordion";
import { ReactComponent as CarManufacturersIcon } from "../assets/directions_car-24px.svg";
import { PermissionInfo } from "@webiny/app-admin/components/Permissions";
// Components for working with forms:
import { Form } from "@webiny/form";
import { Select } from "@webiny/ui/Select";
import { Switch } from "@webiny/ui/Switch";
const t = i18n.ns("app-i18n/admin/plugins/permissionRenderer");
const PERMISSION_NAME = "car-manufacturers";
export default {
type: "admin-app-permissions-renderer",
name: "admin-app-permissions-renderer-car-manufacturers",
render(props) {
// `value` represents an array of all permission objects selected for the
// security group we're currently editing. To apply changes to the `value`
// array, we use the provided `onChange` callback.
const { value, onChange } = props;
// Callback that gets triggered whenever a form element has changed.
// If needed, additional object manipulations can be performed too.
const onFormChange = useCallback(
data => {
// Let's filter out the `car-manufacturer` permission object.
// It's just easier to build a new one from scratch.
const newPermissions = value.filter(item => item.name !== PERMISSION_NAME);
// We only want the permissions object to end up in the `value` array if
// we have a value in `rwd` or `specialFeature` properties.
if (data.rwd || data.specialFeature) {
newPermissions.push(data);
}
// Finally, call the `onChange` callback to assign the permissions
// object into the `value`.
onChange(newPermissions);
},
[value]
);
// Set up default form data, which happens once the security group data
// has been retrieved from the GraphQL API.
const defaultFormData = useMemo(() => {
return value.find(item => item.name === PERMISSION_NAME) || { name: PERMISSION_NAME };
}, [value]);
// We are using a couple of different React components to get the job done:
// - for a nicer UI - AccordionItem, Grid, Cell, and PermissionInfo components
// - for working with forms - Form, Bind, Select, and Switch components
return (
<AccordionItem
icon={<CarManufacturersIcon />}
title={t`Car Manufacturers`}
description={t`Manage Car Manufacturer app access permissions.`}
>
<Form data={defaultFormData} onChange={onFormChange}>
{({ Bind }) => (
<Grid>
<Cell span={6}>
<PermissionInfo title={t`Access Level`} />
</Cell>
<Cell span={6}>
<Grid>
<Cell span={12}>
<Bind name={"rwd"}>
<Select label={t`Access Level`}>
<option value={""}>{t`No Access`}</option>
<option value={"r"}>{t`Read`}</option>
<option value={"rw"}>{t`Read, write`}</option>
<option value={"rwd"}>{t`Read, write, delete`}</option>
</Select>
</Bind>
</Cell>
<Cell span={12}>
<Bind name={"specialFeature"}>
<Switch label={t`Has access to a special feature`} />
</Bind>
</Cell>
</Grid>
</Cell>
</Grid>
)}
</Form>
</AccordionItem>
);
}
} as AdminAppPermissionRendererPlugin;
info

In order to actually compile the code changes we're about to make and see them in browser, we need to run the following Webiny CLI command:

yarn webiny watch apps/admin --env dev

To learn more, check out the Use the Watch Command guide.

Feel free to copy and paste the shown code into your project and use it as a starting point. When doing that, please note the following:

  • the permissions folder is a new folder that we created, it wasn't initially there
  • the code uses a custom directions_car-24px.svg SVG icon which you'll also want to copy into your project (otherwise the build will fail)

After you do that, just make sure that the plugin is actually registered in apps/admin/code/src/plugins/index.ts, otherwise no change will appear in your browser.

If you've been following the previous two tutorials, then you can simply import it via the packages/car-manufacturers/admin-app/src/index.ts file:

packages/car-manufacturers/admin-app/src/index.ts
import { Plugin } from "@webiny/plugins/types";
import menus from "./menus";
import routes from "./routes";
import permissions from "./permissions";
export default (): Plugin[] => [menus(), routes(), permissions];

By completing this step, we've added the new Car Manufacturers permissions section in the security group form. System administrators can now create new (or extend existing) security groups, and give access to our new Car Manufacturers module. For example, one security group can allow only reading car manufacturers data, while the other one may allow all three primary actions - read, write, and delete, and even allow access to the imaginary special feature.

Now that we have this new security permissions section, let's create the new Car Manufacturers security group, that enables full access to our new Car Manufacturers module. We'll need this security group in order to perform some manual testing down the road.

New Car Manufacturers Security Group

Note that, at this point, nothing will actually happen for identities that have or don't have these newly added permissions in their security group. We still need to implement the actual authorization logic, both on the GraphQL API and Admin Area side, which we cover in the following sections.

Content Locales#

While creating the new Car Manufacturers security group, you might have noticed you immediately get to define access on a locale level.

Define Access on a Locale Level

This is because, out of the box, Webiny is a multi-locale system, and as such, it gives you the ability to specify which locales an identity can access. Note that you cannot specify different permissions for different locales. All of the permissions you specify in one or more permission sections will be applied to all allowed locales.

For now, know that we will need to take this into account while performing the authorization checks in the following section. Also, for purposes of this tutorial, we can select All Locales, to allow access across all locales.

FAQ#

Can I assign multiple security groups to a single identity (user)?#

No, you can't. Every identity can be part of exactly one security group. If you are in a situation where you need to assign multiple security groups to an identity, you'll need to create a new security group, that consist of all permissions that are assigned to the initial two.

What does the rwd stand for?#

It stands for read, write, and delete.

Can I define my security permission objects differently from what it's used this tutorial?#

Yes. The rwd and hasSpecialFeature properties are not required and you can define your security permission objects in any way you like or need. For example, if you don't prefer the rwd approach we showed here, you could create standalone hasRead, hasWrite, and hasDelete properties instead.

We just thought the rwd approach is lightweight and simple to understand.

Is it possible to create a checkbox which would immediately allow full access to my module?#

Sometimes, permissions sections may grow in size. And in that case, it might be easier to have a simple checkbox that would enable full access to your module, instead of having the user click through all of the available permissions.

So, if you've reached this point, then it might make sense to extend your security permissions object, with a special boolean fullAccess property, and take that into account while performing backend and frontend authorization (covered in following sections of this tutorial).

Last updated on by Pavel Denisjuk