Role Based Access Control

Introduction to Role-Based Access Control (RBAC)

In this lesson, we'll delve into role-based access control (RBAC), a critical concept in securing applications. RBAC helps you manage user permissions based on their roles. This is important for maintaining security and ensuring that users can access only the data and functionalities they are authorized to use.

As a reminder from the previous lesson, we've already set up basic authentication on our GraphQL server using graphql-ruby and Sinatra. Now, we will build on that foundation to implement more granular access control using roles.

Implementing Role-Based Access Control

To implement RBAC, we need to differentiate between user roles and permissions. For simplicity, we will use two roles: ADMIN and USER. Each role will have different permissions.

Here's an example dataset of users with their respective roles:

UsernamePasswordRole
adminadminADMIN
useruserUSER

Next, let's modify our Sinatra application to extract user roles based on a provided token.

Note on the token scheme: For this demo, the token is simply "Bearer <username>" — the client constructs it from the username returned by the login mutation. The server extracts the username from this header to look up the user and their role. This is not a real authentication mechanism (see the Security Considerations section at the end), but it lets us focus on how role-based authorization works within GraphQL resolvers.

require 'sinatra'
require 'graphql'
require 'json'

USERS = [
  { username: 'admin', password: 'admin', role: 'ADMIN' },
  { username: 'user', password: 'user', role: 'USER' }
]

set :port, 4000

post '/graphql' do
  request.body.rewind
  params = JSON.parse(request.body.read)
  
  token = request.env['HTTP_AUTHORIZATION'] || ''
  
  # Extract the username from a "Bearer <username>" token.
  # We validate the prefix and reject malformed headers.
  user = nil
  parts = token.split(' ')
  if parts.length == 2 && parts[0] == 'Bearer' && !parts[1].empty?
    username = parts[1]
    user = USERS.find { |u| u[:username] == username }
  end
  
  result = MySchema.execute(
    params['query'],
    variables: params['variables'],
    context: { user: user }
  )
  
  content_type :json
  result.to_json
end

This code sets up a Sinatra endpoint on port 4000 with user data and parses the Authorization header from incoming requests to determine the user's identity and role. The USERS array defines users with a username, password, and role. The endpoint validates that the header follows the expected Bearer <username> format — checking that the prefix is "Bearer" and that a non-empty username is present. If the header is missing, blank, or malformed, the user variable remains nil, which downstream resolvers treat as an unauthenticated request. When the header is valid, the endpoint looks up the corresponding user in the USERS array and passes the user object to the GraphQL context for role-based access control.

Securing Mutations with RBAC

Before we dive into the code, let's organize our GraphQL types and mutations using Ruby modules for better code organization. We'll use two modules:

  • Types: Contains all our GraphQL type definitions (like BookType, UserType, QueryType)
  • Mutations: Contains all our mutation classes (like Login, AddBook)

Modules in Ruby act as namespaces, allowing us to group related classes together and avoid naming conflicts. For example, Types::BookType means the BookType class inside the Types module. This is a common pattern in graphql-ruby applications to keep code organized and maintainable.

To secure mutations, we will use the login mutation to authenticate users and assign roles. We will then secure another mutation, addBook, ensuring only ADMIN users can add books.

How our demo token flow works: The login mutation verifies credentials and returns the user's username and role. The client then constructs a token as "Bearer <username>" and sends it in the Authorization header on subsequent requests. The server parses this header to identify the user. In a real application, the server would generate a cryptographically signed token (like a JWT) instead — see the Security Considerations section for details.

module Types
  class BookType < GraphQL::Schema::Object
    field :id, ID, null: false
    field :title, String, null: false
    field :author, String, null: false
  end

  class UserType < GraphQL::Schema::Object
    field :username, String, null: false
    field :role, String, null: false
  end

  class QueryType < GraphQL::Schema::Object
    field :books, [BookType], null: true
    
    def books
      BOOKS
    end
  end

  class MutationType < GraphQL::Schema::Object
    field :login, mutation: Mutations::Login
    field :add_book, mutation: Mutations::AddBook
  end
end

module Mutations
  class Login < GraphQL::Schema::Mutation
    argument :username, String, required: true
    argument :password, String, required: true
    
    field :username, String, null: false
    field :role, String, null: false
    
    def resolve(username:, password:)
      user = USERS.find { |u| u[:username] == username && u[:password] == password }
      
      if user.nil?
        raise GraphQL::ExecutionError, 'Invalid credentials'
      end
      
      { username: user[:username], role: user[:role] }
    end
  end

  class AddBook < GraphQL::Schema::Mutation
    argument :title, String, required: true
    argument :author, String, required: true
    
    field :id, ID, null: false
    field :title, String, null: false
    field :author, String, null: false
    
    def resolve(title:, author:)
      user = context[:user]
      
      if user.nil? || user[:role] != 'ADMIN'
        raise GraphQL::ExecutionError, 'You do not have permissions to add a book'
      end
      
      new_book = { id: SecureRandom.uuid, title: title, author: author }
      BOOKS << new_book
      new_book
    end
  end
end

BOOKS = [
  { id: '1', title: 'The Hobbit', author: 'J.R.R. Tolkien' },
  { id: '2', title: 'Harry Potter', author: 'J.K. Rowling' }
]

class MySchema < GraphQL::Schema
  query Types::QueryType
  mutation Types::MutationType
end

In this setup, we check if the user role is ADMIN before allowing them to add a book. If the user does not have the necessary permissions, a GraphQL::ExecutionError is raised. We also use SecureRandom.uuid to generate a unique ID for each new book, avoiding duplicate IDs when multiple books are added.

Testing Role-Based Access Control: Login

Testing Role-Based Access Control: Add and Fetch Books

Testing Role-Based Access Control: Putting All Together

Finally, let's put things together and call all these methods we've defined:

token = login('admin', 'admin')
if token.nil?
  puts 'Failed to login'
  exit
end

puts 'Fetching books...'
books = get_books(token)
puts "Books: #{books}"

puts 'Adding a new book...'
new_book = add_book('1984', 'George Orwell', token)
puts "Added book: #{new_book}"

puts 'Fetching books again...'
updated_books = get_books(token)
puts "Books: #{updated_books}"

This example script demonstrates a complete flow: logging in as admin, adding a book, and fetching the list of books to validate the role-based access control implementation.

Important Security Considerations

⚠️ The authentication and authorization implementation shown in this lesson is for educational purposes only and is NOT production-ready. Let's discuss what we did and why it's insecure:

What We Implemented (Insecure Demo Patterns)

  1. Plaintext passwords: We stored passwords as plain text in the USERS array
  2. Username as token: The token is simply 'Bearer <username>', which can be easily forged
  3. No token validation: We extract the username from the token without any cryptographic verification
  4. No expiration: Tokens never expire
  5. Hard-coded user data: User credentials and roles are stored directly in the code
  6. Client-constructed tokens: The server doesn't issue tokens — the client fabricates them from the username, meaning anyone who knows a username can impersonate that user

Why This Is Insecure

  • Anyone can forge tokens: Since the token is just 'Bearer admin', anyone can create it without actually logging in
  • No authentication: We're not verifying that the user actually provided valid credentials — we just trust whatever username is in the token
  • Passwords are readable: Anyone with access to the code can see all passwords
  • No token tracking: There's no way to invalidate or revoke tokens (logout)
  • Role escalation risk: A user could change their token from 'Bearer user' to 'Bearer admin' to gain admin privileges

Production-Ready Alternatives

For real applications, you should implement:

1. Password Hashing with bcrypt:

require 'bcrypt'

# When creating a user
hashed_password = BCrypt::Password.create('admin')

# When verifying login
user = USERS.find { |u| u[:username] == username }
if user && BCrypt::Password.new(user[:password_hash]) == password
  # Valid credentials
end

2. JWT Tokens with Role Claims:

require 'jwt'

# Generate a token with user info and role
payload = {
  user_id: user[:id],
  username: user[:username],
  role: user[:role],
  exp: Time.now.to_i + 3600  # Expires in 1 hour
}
secret = ENV['JWT_SECRET']
token = JWT.encode(payload, secret, 'HS256')

# Verify and decode token in your GraphQL context
begin
  decoded = JWT.decode(token, secret, true, { algorithm: 'HS256' })
  user_data = decoded[0]
  # Use user_data['role'] for authorization
rescue JWT::DecodeError
  # Invalid or expired token
end

3. Proper Authorization Checks:

# Use a dedicated authorization library
class AddBook < GraphQL::Schema::Mutation
  def resolve(title:, author:)
    user = context[:current_user]
    
    # Verify user is authenticated
    raise GraphQL::ExecutionError, 'Authentication required' unless user
    
    # Verify user has the required role
    raise GraphQL::ExecutionError, 'Admin access required' unless user.admin?
    
    # Proceed with mutation
  end
end

4. Use Authorization Libraries:

  • Pundit: Policy-based authorization
  • CanCanCan: Role-based authorization with ability definitions
  • Action Policy: Modern authorization framework

5. Database-Backed User Management:

# Store users in a database with proper schema
class User < ActiveRecord::Base
  has_secure_password  # Uses bcrypt automatically
  
  enum role: { user: 0, admin: 1 }
  
  def admin?
    role == 'admin'
  end
end

Understanding the Token Pattern Difference

You may have noticed that this lesson uses a different token pattern than the previous lesson:

  • Previous lesson (Authentication): Checked for static 'Bearer token'
  • This lesson (RBAC): Uses 'Bearer <username>' to identify different users

Both patterns are insecure demonstrations designed to teach concepts:

  • The first lesson focused on where to check authentication
  • This lesson focuses on how to use user identity for authorization

In production, you would use a single, secure token system (like JWT) that encodes user identity, roles, and expiration in a cryptographically signed token.

Key Takeaway

This lesson focused on understanding how role-based access control works in GraphQL — how to pass user context, check roles in resolvers, and enforce permissions. The implementation details were intentionally simplified to focus on these authorization concepts. In your production applications, always use proper password hashing, cryptographically secure tokens with claims, and established authorization libraries.

Lesson Summary

In this lesson, we successfully implemented role-based access control (RBAC) using graphql-ruby and Sinatra. You learned how to:

  • Set up a GraphQL server with graphql-ruby and Sinatra.
  • Implement basic authentication and role-based authorization.
  • Secure GraphQL mutations with RBAC.
  • Test the implementation using practical examples.

Next, you'll engage in practice exercises to reinforce these concepts. These hands-on activities will help solidify your understanding of RBAC and prepare you for more advanced topics. Keep exploring and experimenting with different scenarios to deepen your knowledge of securing GraphQL APIs.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal