More Authorization with Pundit
Ok the whole reason why I wrote my previous post on authorization with Pundit was so that I could talk about more advanced usages of Pundit in our application.
The Learn app started as a student facing curriculum management system with very simple admin interfaces. The admin tasks included adding students to certain batches and deploying curriculum out to them. However as our use cases grew, the complexity of the admin tasks grew as well. Until finally, we ended up with two large admin apps, the Organizations app and the Curriculum app (among other small admin UIs).
We now have a third admin app in development and, though it’s tied to the students, it no longer has the concept of student’s batches. This poses a new kind of challenge since we haven’t been doing authorization by apps. We’ve been inferring the authorization of an app by the combination of the user’s role and the object they have the role for.
With that in mind, let’s talk about a solution to handle app specific authorization.
Creating the concept of sub-apps
Since our current implementation requires us to associate a user + role + object, we need to create the concept of sub-apps. The way that I addressed this was to create a new table in our Rails app called sub_apps
and an associated model.
Create user roles for the sub-apps
We now have all three parts of user + role + object (sub-app), so we can go forward with creating some user roles. For brevity, we’ll only focus on one of the apps. I just threw this into my rakefile
:
Running the above task should build an admin user role for AdminUser
and a deployer role for AdminUser
, both for the curriculum app.
Configuring our policy object
Next, we’ll want to configure a policy object for our SubApp
objects called SubAppPolicy
. In app/policies/
create a file called sub_app_policy.rb
and create some general authorization rules for the sub apps:
Remember from the previous post about Pundit
that the policy objects take in a user and a record object. Each of the instance methods defined can be used in the controller to authorize a particular action:
Great, now we can use the SubAppPolicy
to authorize users. So another example might be authorizing a user to deploy curriculum out to students:
Hmm… that particular implementation feels weird though. All SubApps
should have an authorization method called deploy_access?
. It would be confusing for anyone working with this code in the future to figure out where I’m authorizing the user to deploy content.
Method delegation to other policy objects
Remember that at the core of it, the authorize
helper method is doing the following things:
- Get the class of the object and instantiate the policy object for that class. For our particular case:
- Send the method passed in as the second argument:
- If it returns a truthy value, then allow, otherwise raise an authorization error.
Armed with this knowledge, we can delegate missing methods out to more sub app specific policy objects.
When implementing something like this, I usually like to whitelist the objects that the method calls will be delegated out to. In order to do so, let’s just create a mapping:
Next, we’ll override the method_missing
method for the SubAppPolicy
objects to try to delegate out to the specific app policy objects if allowed.
Cool, now all we have to do is set up the deploy_access?
method in the SubApp::CurriculumPolicy
.
Friendly errors
Almost done! The last thing we want to do is that if the specific app policy object also doesn’t know how to respond to the method, we want to give a more descriptive error message. We can rescue a NoMethodError
and add our own messaging to it:
With the above custom error, our message will look something like SubAppPolicy::MethodDelegationError: undefined method "some_crazy_method_name" for <SubAppPolicy> and <SubApp::CurriculumPolicy>
Conclusion
Now that we have this in place, it should be more easy to find app specific authorization calls as well as general authorization across the apps.
Note that there are definitely other ways to go about solving this issue, like starting to create separate apps as opposed to a monolithic Rails app, but given the constraints and requirements, this was the best way to tackle the issue in this case.