- Read Tutorial
With Pundit installed now we're able to start configuring permissions (also known as policies) for our application to have.
Let's start with a basic use case:
- A
post
should only be able to be able to beedited
by theuser who created
thepost
I highlighted the terms: post
, edited
, and user who created
, this is because when you're building a permission structure, it's important to make it as straightforward as possible. Authorization features can get messy very quickly, mainly due to developers not spending the time in the beginning to outline exactly how the permission should be configured. It's for this reason why I like using the Pundit gem for integrating authorization, due to its minimal nature.
When it comes to building a permission structure feature, ensuring that you have a comprehensive test suite is very important since it will clearly illustrate what scenarios are covered.
Using Pundit to Integrate an Edit Policy
We already have tests in the spec/features/post_spec.rb
file, so we don't have to create new specs. The change we'll be working on is moving authorization away from the User
class and instead we'll manage it from various policies
.
Let's begin by creating a new file that will store our policies that are specific to posts
:
touch app/policies/post_policy.rb
With that file created we can define a class that inherits from the ApplicationPolicy
class and we'll integrate a sample method for managing permissions for the posts
update
action:
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def update? record.user_id == user.id end end
In our PostPolicy
class we're overriding the update?
method originally declared in the ApplicationPolicy
since it simply returns false
(side note: this means that it will block every update
action that it's added to throughout the application. The ApplicationPolicy
class is mainly in place to give a structure/naming structure that we can use to override with our own unique requirements).
Our record.user_id == user.id
call is simply stating that the only user that should be able to update
a post
is the user
that created the post
. If you're wondering where the record
and user
attributes are coming from, if you look at the ApplicationPolicy
class you'll see that they're set as read only
attributes and they represent whatever object that we're adding authorization to, such as a post
in our app, and then the user
. This is a great benefit to using Pundit since it gives us easy access to the items that we want to work with. When we add a TopicPolicy
we'll be able to use very similar code to how we're working with posts
.
Let's also override the edit?
method:
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def edit? record.user_id == user.id end def update? record.user_id == user.id end end
If you're getting a bad feeling about this code, that's good, it's never a good idea to duplicate code, so let's DRY up our record.user_id == user.id
calls and refactor them into their own method:
# app/policies/post_policy.rb class PostPolicy < ApplicationPolicy def edit? user_who_can_access_post end def update? user_who_can_access_post end def user_who_can_access_post record.user_id == user.id end end
This may seem like a counterintuitive refactor since we now have MORE code than we did before. However this is a better approach since:
- It removes the code duplication
- If we want to add other authorized users in the future, such as editors or admins we can easily make the change in a single method instead of having to make the same change in multiple places.
- The main reason I like this refactor is because it's very explicit and it reads better than before. If you took over an application, you may not immediately understand what the code
record.user_id == user.id
means, however the methoduser_who_can_access_post
clearly describes the intent of the program flow.
Now let's refactor our PostsController
, by using Pundit policies we can now get rid of some ugly authorization code in our edit
action and then integrate a proper permission structure to the update
action (which currently has no protection from unauthorized HTTP requests). The two methods should now look like this:
# app/controllers/topics/posts_controller.rb def edit authorize @post end def update @post = @topic.posts.find(params[:id]) authorize @post if @post.update(post_params) redirect_to topic_post_path(topic_id: @post.topic_id, id: @post), notice: 'Your post was successfully updated.' else render :edit, notice: 'There was an error processing your request!' end end
I like this implementation much more than what we had before. If you run the specs you'll see that we have a failure, however it's not with the authorization feature itself, instead it has to do with the test expecting to send a user back to the post
page.
By default Pundit simply raises an exception when a user attempts to access a page they're not authorized to view, let's test this out in the browser by creating a new user (that doesn't have any posts) and then try to edit a post.
So here we need to rescue the Pundit::NotAuthorizedError
exception and tell the application how to handle it. We can do this in the ApplicationController
like so:
# app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Pundit rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized protect_from_forgery with: :exception def current_user super || OpenStruct.new(full_name: 'Guest') end private def user_not_authorized flash[:alert] = "You are not authorized to access this page." redirect_to(request.referrer || root_path) end end
Now if we navigate back to the website and test it out you'll see we're redirected to the homepage.
Let's also refactor our failing test so it expects the unauthorized user to be sent to the homepage instead of the post page:
# spec/features/post_spec.rb:109 it 'does not allow a user to edit a post they did not create' do logout(:user) login_as(@second_user, :scope => :user) visit edit_topic_post_path(topic_id: @topic.id, id: @post.id) expect(current_path).to eq(root_path) end
With that change you'll see that all of the specs are back to passing and now our application is blocking unauthorized users from editing posts and our code is more streamlined than it was before.
Another benefit to the code we now have implemented is that the old implementation didn't block API requests, which means that hackers may have been able to edit posts using outside services. We blocked users from accessing the edit
page on the website, but a HTTP request could have called the application server and passed in data to edit a post that they did not create. With the new policy we're properly blocking both web and HTTP requests.
Also, I hope you appreciated that having the authorization tests in place from earlier in the application build made it possible to be confident that our new permission structure is working properly. Since I knew we were already verifying that only authorized users were able to edit posts in the specs we were able to clean up the implementation with minimal changes to the RSpec tests, which is the sign of a good test suite.
You may have noticed that we're not rendering notifications to users. When a user tries to access a page they don't have authorization for, they should be shown a message such as "You are not authorized to access this page."
, however they're simply being redirected. In the next guide we'll implement notifications throughout the application.