A JSON based Authorization.
Why not cancancan?
I have been working with cancan/cancancan for years. Since the beginning with database access. After a while, I realised I built a couple of abstractions around ability class and suddenly migrated to JSON for better performance. As I need a full role admin I decided to start to extract this logic to a gem.
Add the following code on your Gemfile and run bundle install:
gem 'authorizy'Run the following task to create Authorizy migration and initialize.
rails g authorizy:installThen execute the migration to adds the column authorizy to your users table.
rake db:migrateclass ApplicationController < ActionController::Base
include Authorizy::Extension
endAdd the authorizy filter on the controller you want enables authorization.
class UserController < ApplicationController
before_action :authorizy
endThe column authorizy is a JSON column that has a key called permission with a list of permissions identified by the controller and action name which the user can access.
{
permissions: [
[users, :create],
[users, :update],
}
}You can change the default configuration.
Alias is an action that maps another action. We have some defaults.
| Action | alias |
|---|---|
| create | new |
| edit | update |
| new | create |
| update | edit |
You can add more alias, for example, all permissions for action index will allow access to action gridy of the same controller. So users#index will allow users#gridy too.
Authorizy.configure do |config|
config.aliases = { index: :gridy }
endSometimes we need to allow access in runtime because the permission will depend on the request data and/or some dynamic logic. For this you can create a Cop class, that inherits from Authorizy::BaseCop, to allow it based on logic. It works like a Interceptor.
First, you need to configure your cop:
Authorizy.configure do |config|
config.cop = AuthorizyCop
endNow creates the cop class. The following example will intercept all access to the controller users_controller:
class AuthorizyCop < Authorizy::BaseCop
def users
return false if action == 'create'
return false if controller == 'users'
return true if current_user == User.find_by(admin: true)
return true if params[:allow] == 'true'
return true if session[:logged] == 'true'
end
endAs you can see, you have access to a couple of variables: action, controller, current_user, params, and session.
When you return false, the authorization will be denied, when you return true your access will be allowed.
If your controller has a namespace, just use __ to separate the modules name:
class AuthorizyCop < Authorizy::BaseCop
def admin__users
end
endIf you want to intercept all request as the first Authorizy check, you can override the access? method:
class AuthorizyCop < Authorizy::BaseCop
def access?
return true if current_user.admin?
end
endBy default Authorizy fetch the current user from the variable current_user. You have a config, that receives the controller context, where you can change it:
Authorizy.configure do |config|
config.current_user = -> (context) { context.current_person }
endYou can allow access to one or more controllers and actions based on your permissions. It'll consider not only the action, like aliases but the controller either.
Authorizy.configure do |config|
config.dependencies = {
payments: {
index: [
['system/users', :index],
['system/enrollments', :index],
]
}
}
endSo now if a have the permission payments#index I'll receive more two permissions: users#index and enrollments#index.
By default the permissions are located inside the field called authorizy in the configured current_user. You can change how this field is fetched:
Authorizy.configure do |config|
@field = ->(current_user) { current_user.profile.authorizy }
endWhen authorization fails and the request is not a XHR request a redirect happens to / path. You can change it:
Authorizy.configure do |config|
config.redirect_url = -> (context) { context.new_session_url }
endYou can use authorizy? method to check if current_user has access to some controller and action.
Using on controller:
class UserController < ApplicationController
before_action :assign_events, if: -> { authorizy?('system/events', 'index') }
def assign_events
end
endUsing on view:
<% if authorizy?(:users, :create) %>
<a href="/users/new">New User</a>
<% end %>Using on jBuilder view:
json.create_link new_users_url if authorizy?(:users, :create)To test some routes you'll need to give or not permission to the user, for that you have to ways, where the first is give permission to the user via session:
before do
sign_in(current_user)
session[:permissions] = [[:users, :create]]
endOr you can put the permission directly in the current user:
before do
sign_in(current_user)
current_user.update(permissions: [[:users, :create]])
endWe have a couple of check, here is the order:
Authorizy::BaseCop#access?;session[:permissions];current_user.authorizy['permissions'];Authorizy::BaseCop#controller_name;
If you have few permissions, you can save the permissions in the session and avoid hit database many times, but if you have a couple of them, maybe it's a good idea save it in some place like Redis.
It's a good idea you keep your permissions in the database, so the customer can change it dynamic. You can load all permissions when the user is logged and cache it later. For cache expiration, you can trigger a refresh everytime that the permissions change.
Inside database you can use the following relation to dynamicly change your permissions:
plans -> plans_permissions <- permissions
|
v
role_plan_permissions
^
|
rolesYou can test you app passing through all authorizy layers:
user = User.create!(permission: { permissions: [[:users, :create]] })
expect(user).to be_authorized(:users, :create)Or make sure the user does not have access:
user = User.create!(permission: {})
expect(user).not_to be_authorized(:users, :create)