- Introduction
- Overview of Role-Based Access Control (RBAC)
- User Roles
- Defining User Roles
- Assigning Roles to Users
- Working with Context and Applying Permissions
- Configuring the Context
- Applying Permissions with Access Control Lists (ACL)
- The ACL Object
- Cumulative Permissions
- Default Denial
- Superuser Role
- Securing Custom Queries and Mutations:
- Steps To Add Authorisation to Graphweaver
- Implementing Column Level Security with Hooks
- Conclusion
Introduction
The purpose of this article is to provide an overview of the authorization mechanism in Graphweaver and explain how to configure it for row-level security (RLS) and column-level security (CLS).
We will discuss user roles, the Authorization Context, ACLs, and how to secure custom queries and mutations.
Let’s start with how Graphweaver uses RBAC.
Overview of Role-Based Access Control (RBAC)
Role-Based Access Control (RBAC) is a common approach to implementing access control based on user roles.
RBAC allows you to define permissions and access rights at a role level and assign those roles to users. In Graphweaver, RBAC can be implemented using access control lists (ACLs) and hooks.
ACLs define the permissions for different roles on specific resources or operations. You can specify the access rules for different roles using the ACL syntax.
Here's an example of how RBAC can be implemented using an ACL:
const acl: AccessControlList<Task, Context> = {
ADMIN: {
// Admins have full access to all tasks
all: true,
},
USER: {
// Users can only perform operations on their own tasks
all: (context) => ({ user: { id: context.user.id } }),
},
MODERATOR: {
// Moderators have additional permissions
// For example, they can read all tasks but cannot create or update them
read: true,
},
};
In the above example, the ACL specifies the access rules for the Task
entity based on different roles.
The ADMIN
role has unrestricted access (all: true
), while the USER
role can only perform operations on their tasks. The MODERATOR
role has read-only access to all tasks. This ACL is then applied to the relevant entities.
By default when using authorisation, Graphweaver applies Implicit Deny to every entity in the system. This means that by default there are no permissions to any entity. and it is up to you to then apply the ACL as needed to each entity.
However, there are times when this is not desirable and it is possible to configure Graphweaver to Implicit Allow mode. This will then by default allow access to all entities and then you can apply ACL to entities to lock it down.
You can set implicit allow like this:
export const graphweaver = new Graphweaver<AuthorizationContext>({
resolvers,
apolloServerOptions: {
plugins: [authApolloPlugin(addUserToContext, { implicitAllow: true })],
},
})
By utilizing RBAC and ACLs in Graphweaver, you can easily control access to resources and operations based on user roles, ensuring that users only have the permissions necessary for their designated roles.
To implement RBAC in Graphweaver there are several steps to perform:
- Create the Roles used by the system
- Create users and assign them to roles
- Map the users to roles inside the context when making an API request
- Apply the ACL on the entities being accessed.
Let’s start with user roles.
User Roles
User roles play a vital role in managing access and permissions within Graphweaver. They allow you to define different levels of authority and control over the resources and functionalities available to users.
In Graphweaver, you can leverage user roles to implement fine-grained access control and enforce security measures.
Defining User Roles
When working with user roles in Graphweaver, it's common to have a set of constants that represent the available roles. For example, let's consider the following set of roles:
enum Roles {
ADMIN = 'ADMIN',
USER = 'USER',
MODERATOR = 'MODERATOR',
}
In this example, we have three roles defined: ADMIN
, USER
, and MODERATOR
. You can extend or modify this set based on the specific requirements of your application.
Next, let’s assign a role to a user.
Assigning Roles to Users
Once you have defined the roles, you need to assign them to users based on their privileges or responsibilities.
This assignment typically occurs during user registration or account management processes.
For instance, when a new user registers, you can provide an interface or form where an administrator or a user with appropriate permissions can assign the desired role to the user.
The chosen role can be stored in the user's profile or database record.
Once we have the roles defined and the users attached to roles we need to let the Graphweaver know which roles the user making the current request has. To do this we must use the context.
Working with Context and Applying Permissions
In Graphweaver, the context
plays a crucial role in implementing authorization and applying permissions based on user roles. The context
object contains user-related information and is utilized to enforce access control rules throughout the API.
Configuring the Context
In the example code snippet provided, the context is configured within the handler
function. Let's take a closer look at how it is set up:
context: async ({ event }: LambdaContextFunctionArgument<any>) => {
// Let's use the x-user-id to specify a different user for testing
// In a real-world application, this would be inside an access token
const userId = (event as any)?.headers?.['x-user-id'] ?? '1';
const user = User.fromBackendEntity(
await BaseLoaders.loadOne({ gqlEntityType: User, id: userId })
);
if (!user) throw new Error('Bad Request: Unknown user id provided.');
const context: Context = {
user,
// If the user id is 4, this is Darth Vader and we return the dark side role
roles: user.name === 'Darth Vader' ? [Roles.DARK_SIDE] : [Roles.LIGHT_SIDE],
};
upsertAuthorizationContext(context);
return context;
},
In this example, the context is retrieved from the incoming event
object, specifically from the 'x-user-id'
header. This allows specifying a different user for testing purposes. In a real-world scenario, the user information would typically be extracted from an access token or authentication mechanism.
The context
object consists of two main properties:
user
: Represents the authenticated user, retrieved based on the provided user ID.roles
: Specifies the roles associated with the user. In this case, if the user's name is "Darth Vader," theRoles.DARK_SIDE
role is assigned; otherwise, theRoles.LIGHT_SIDE
role is assigned.
Once the context is defined, it is important to call the upsertAuthorizationContext(context)
function to ensure that the authorization context is properly set for the subsequent authorization checks.
Let’s recap what we have done so far, we have:
- Created the roles in our application
- Assigned users to roles
- Then updated the context to have access to the roles for the current user.
Now we have everything we need to start applying permissions.
Applying Permissions with Access Control Lists (ACL)
Graphweaver provides a mechanism called Access Control Lists (ACL) to define and apply permissions based on user roles.
Let’s look at an example using an ACL for the Task
entity:
const acl: AccessControlList<Task, Context> = {
LIGHT_SIDE: {
// Users can only perform operations on their own tasks
all: (context) => ({ user: { id: context.user.id } }),
},
DARK_SIDE: {
// Dark side user role can perform operations on any tasks
all: true,
},
};
The acl
object defines permissions for two different roles: LIGHT_SIDE
and DARK_SIDE
.
Within each role, you can specify the permissions for different operations. In this example, users with the LIGHT_SIDE
role can only perform operations on their own tasks, while users with the DARK_SIDE
role have unrestricted access to all tasks.
To apply the defined ACL to the Task
entity, Graphweaver provides the @ApplyAccessControlList
decorator.
It is applied to the Task
class as follows:
@ApplyAccessControlList(acl)
@ObjectType('Task')
export class Task extends GraphQLEntity<OrmTask> {
// ...
}
By applying the @ApplyAccessControlList
decorator to the Task
class, the permissions defined in the ACL are enforced for all operations involving the Task
entity.
Let’s look into the ACL object in more detail.
The ACL Object
Each property at the top level of the ACL object corresponds to a user role.
Permissions can be defined for operations like read
, create
, update
, and delete
. Additionally, two convenience operations are available: write
(includes create
, update
, and delete
) and all
(includes all four operations).
Permissions can be set to true
for unrestricted access or defined using filter functions for fine-grained control.
Example:
@ApplyAccessControlList({
ACCOUNT_ADMINISTRATOR: {
read: (context: AuthorizationContext) => ({
id: {
$in: context.mappedAccounts,
},
}),
},
// ...
})
export class Account extends GraphQLEntity<CrmAccount> {
// ...
}
Cumulative Permissions
ACLs can have multiple rules that apply in different scenarios. Permissions accumulate when a user belongs to multiple roles or when an ACL specifies permissions for Everyone
along with individual roles. Relevant permissions are combined cumulatively.
Default Denial
If no ACL is specified for an entity, access is denied for all users regardless of their role.
Superuser Role
A special role called the administrative user role, or superuser, has full access permissions to every entity, overriding ACL restrictions.
The administrator role name can be set using the setAdministratorRoleName()
function during API startup.
Securing Custom Queries and Mutations:
Authorisation inside custom queries/mutations must be implemented by the developer, and ACL restrictions apply only to the autogenerated queries / mutations.
Steps To Add Authorisation to Graphweaver
To enable authorisation security in Graphweaver, you need to follow these steps:
- Add ACLs for each entity: Define ACLs for each GraphQL entity in the project. Decorate the entity classes with the
@ApplyAccessControlList
decorator and specify the permissions for each role and operation. - Update the logic for mapping user roles: Add logic that maps user roles into the
Context
object, fetching the actual roles assigned to the user. - Update the logic for any other mappings: Similarly, update the logic that maps other user info you may need to make in the
context
for example you may allow a user to modify some related entity. - Add logic to secure custom queries and mutations: Review all custom queries and mutations to ensure that the security requirements are properly implemented. Make sure that any logic accessing data outside of data loaders or data providers also respects the access restrictions defined in the ACLs.
By following these steps, you can configure and enable security authorisation in Graphweaver.
Implementing Column Level Security with Hooks
In Graphweaver, hooks can be utilised to implement column-level security, allowing fine-grained control over the fields and columns that users can access or modify based on their roles and permissions.
By leveraging hooks, you can customise the behaviour of specific fields or columns within an entity.
Here's an example of how hooks can be used for column-level security:
@ObjectType('Task')
export class Task extends GraphQLEntity<OrmTask> {
// ...
@Field(() => String)
@Hook(HookRegister.BEFORE_READ)
async beforeRead(params: ReadHook) {
// Check if the user has permission to access the description field
if (!this.canAccessDescriptionField(params.context)) {
throw new Error('Access Denied');
}
return params;
}
@Field(() => String)
@Hook(HookRegister.BEFORE_UPDATE)
async beforeUpdate(params: CreateOrUpdateHook) {
// Check if the user has permission to modify the description field
if (!this.canModifyDescriptionField(params.context)) {
throw new Error('Access Denied');
}
return params;
}
// ...
private canAccessDescriptionField(context: Context): boolean {
// Implement your custom logic to determine if the user can access the description field
// Example: Check the user's role or other conditions
return context.roles.includes(Roles.LIGHT_SIDE);
}
private canModifyDescriptionField(context: Context): boolean {
// Implement your custom logic to determine if the user can modify the description field
// Example: Check the user's role or other conditions
return context.roles.includes(Roles.DARK_SIDE);
}
}
In the above code, the Task
entity has a description
field for which column-level security is desired.
Two hooks are added: beforeRead
and beforeUpdate
- In the
beforeRead
hook, thecanAccessDescriptionField
method is called to determine if the user has permission to access thedescription
field. If the user doesn't have the necessary permissions, the hook returns anError
. - In the
beforeUpdate
hook, thecanModifyDescriptionField
method is invoked to check if the user can modify thedescription
field. If the user doesn't have the required permissions, the hook returns anError
.
By implementing custom logic within these hooks, you can enforce column-level security based on user roles, permissions, or any other criteria you define.
This provides a powerful mechanism to control access and modification rights at a granular level within your entities.
Conclusion
In conclusion, implementing authorisation in Graphweaver is essential for ensuring secure access control to resources and functionalities.
By leveraging RBAC (Role-Based Access Control), ACLs (Access Control Lists), and hooks, you can define and enforce permissions based on user roles.
The process involves creating user roles, assigning roles to users, mapping roles within the context, and applying ACLs to entities.
The context plays a crucial role in implementing authorisation and applying permissions. Custom queries and mutations can be secured using custom developer implemented logic.
Additionally, hooks can be utilised to implement column-level security, allowing control over specific fields or columns based on user roles and permissions.
By following the necessary steps, including defining ACLs, mapping user roles, securing custom operations, and reviewing security requirements, you can effectively configure and enable authorisation security in Graphweaver.
Overall, implementing authorisation in Graphweaver helps maintain the integrity of your application by ensuring that users have appropriate access levels and protecting sensitive data and functionalities from unauthorised access.
With these measures in place, you can enhance the security and control of your Graphweaver application, providing a reliable and safe user experience.