When I started Business Class I wanted to do as few choices for the user as possible. I didn't want to discourage people that might prefer an alternative library. One result of that was that I haven't included an authorization layer of any kind. But version 2.0 changed that and I built a proper authorization layer with Action Policy.
Saying we didn't have authorization might not be 100% accurate, but it was treated as authentication. Just a before action filter to check if the user is an admin or not. And honestly that's kind of enough for what's in the template. But as you'll build your application you are probably going to want to have something better.
One such place was the generator. I was significantly improving the CRUD generator and it felt wrong not to already include authorization handling. Why? Because I wanted to generate files you can just commit to source control. Something already complete. I didn't want to generate something only to go and update it immediately. Same with the tests.
So I had a look around on what might be the best authorization library of the day. I realized that I had starred Action Policy on GitHub and gave it a deeper look. And although I never used it at work, I immediately liked it.
Action Policy is basically Pundit, but better. It has very little magic and comes with GraphQL integration. Funny enough, I don't like GraphQL and try to stay away from it. But if I'll need to use GraphQL it's better if it's with a policy framework that can work with it.
Here's a example policy file generated by Business Class:
class BlogPolicy < ApplicationPolicy
def update?
space_owned? || user&.admin?
end
def destroy?
# deny! unless record.deletable?
update?
end
end
A minimal policy that let's the tenant users to modify the blog, as well as allowing an administrator to do so. Note that Space
model is the tenant and can be renamed to what you want (Tenant
, Team
). And here's the test:
require "test_helper"
class BlogPolicyTest < ActiveSupport::TestCase
setup do
@record = blogs(:one)
@space = spaces(:owners)
@unauthorized_space = spaces(:freelancer)
@admin = users(:chris)
@owner_policy = BlogPolicy.new(@record, space: @space)
@unauthorized_policy = BlogPolicy.new(@record, space: @unauthorized_space)
@admin_policy = BlogPolicy.new(@record, user: @admin)
end
test "#update?" do
assert @owner_policy.apply(:update?)
assert_not @unauthorized_policy.apply(:update?)
assert @admin_policy.apply(:update?)
end
test "#destroy?" do
assert @owner_policy.apply(:destroy?)
assert_not @unauthorized_policy.apply(:destroy?)
assert @admin_policy.apply(:destroy?)
end
end
This little policy is now ready to use and very easy to update to whatever is needed.
You might be wondering how the actual code using the policy looks like. So here's the authorize!
method in the controller:
class BlogsController < SpaceScopedController
# ...
# GET /spaces/:space_slug/blogs/1/edit
def edit
authorize! @blog, to: :update?
end
# ...
# GET /spaces/:space_slug/blogs/1/preview_destroy
def preview_destroy
authorize! @blog, to: :destroy?
render "preview_destroy", layout: false
end
# DELETE /spaces/:space_slug/blogs/1
def destroy
authorize! @blog
@blog.destroy!
redirect_to space_blogs_path, notice: "Blog was successfully destroyed.", status: :see_other
end
# ...
end
If the policy method is matching the action name, all works automatically, otherwise we have to provide the to:
argument. In a way, Business Class doesn't try to do too much, but it's a time saver to start with policy files in place.
Also everything else in the template was upgraded to use Action Policy for consistency.
I talked about rethinking every single decision for the future version 3.0, but I am confident that Action Policy will stay. It's haven't faced any issues with the library and it's nice put all policy rules to a single place.