Ingress
A simple role based authorization framework inspired by CanCan (similar syntax) with a nicer interface for defining the permissions for the roles in your system.
The biggest problem I had with CanCan was the fact that it mostly forced you define the permissions for all the roles in one class (really one method). And when the set of permissions in your system grew very large, you had to bend over backwards to allow you to break things down.
In the OO world we're used to being able to break down functionality into multiple smaller classes which we can them compose into a greater whole. This is the main idea behind this gem, keep the nice syntax that CanCan had, but allow composing the main permission object in your system from many smaller classes.
Installation
Add this line to your application's Gemfile:
gem 'ingress'
And then execute:
$ bundle
Or install it yourself as:
$ gem install ingress
Usage
Let's say you have a user object in your system and the user can have multiple roles. Our set of roles will be guest
, member
, admin
.
First we create the main permission object in our system, let's call it UserPermissions
:
class UserPermissions < Ingress::Permissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
A couple of things of note is that it inherits from Ingress::Permissions
, from which it inherits the initializer:
attr_reader :user
def initialize(user)
@user = user
end
So this object is always instantiated with a user. The second thing to note is that we have to provide a method called
user_role_identifiers
which needs to return a list of role identifier that this particular user has. Above we make the
assumptions that a user has many roles and that a role has a name. So we iterate over the roles collect the symbolized names
and return them. This is essentially what ties everything together. We haven't defined any permissions just yet, but we
can already do the following:
= UserPermissions.new(user)
.can?(:do, :stuff) # returns false
So now we have an object that we can instantiate anywhere, and ask it if our user has a particular permission.
Let us now define some permissions for a role. We'll start with the guest role. First, let's update our UserPermissions
object:
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
We've now said that the permission for the role with the guest
identifier live in the GuestPermissions
class. Let's create
it:
class GuestPermissions < Ingress::Permissions
do
can :view, :non_sensitive_info
can [:create], :session
end
end
It's pretty self explanatory, the class again inherits from Ingress::Permissions
as that's where the simple DSL for defining
permissions lives. The thing to note is that we called the class GuestPermissions
, but it could be called anything, the permissions
we define here are not attached to any role. They only get attached to the role via the define_role_permissions :guest, GuestPermissions
line in the UserPermissions
class. The syntax for defining permissions is:
can 'action', 'subject'
or
cannot 'action', 'subject'
Similar to CanCan, the action
can be any string, symbol or array of strings or symbols. The subject
can also be a string or symbol, or
it can be a class constant. Let's define permissions for the next role in our system, member
which is more complex. Firstly, update our UserPermissions
.
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
:member, MemberPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
Simple, next MemberPermissions
class:
class MemberPermissions < Ingress::Permissions
do
can [:show, :update, :destroy], :session
can :accept, :terms
can [:view, :create], Post
can [:update, :destroy], Post, if: ->(user, post) do
user.id == post.user_id
end
end
end
It's a little bit more complex, but still fairly self explanatory. As you can see, we have a Post
object in our system. So we allow
user with a member
role to view and create posts, and they can update and destroy posts that they own. So we could do:
= UserPermissions.new(user)
.can?(:create, Post) # returns true
post = user.posts.first # assume we can get the list of posts form the user object
.can?(:update, post) # returns true
The condition lambda always takes two parameters, the user
and an object
, the object is whatever we supply to the can?
method,
when we check permissions.
Let's add our admin role:
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
:member, MemberPermissions
:admin, AdminPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
And the class:
class AdminPermissions < Ingress::Permissions
do
can "*", "*" # you can also use can_do_anything
end
end
As you can see both action
and subject
can be wildcards, so in this case an admin would be able to do anything in the system, i.e.
any call to can?
will always return true
.
So what else can we do? Well let's say we wanted another role called limited_admin
which would be similar to admin, but can't destroy
comments:
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
:member, MemberPermissions
:admin, AdminPermissions
:limited_admin, LimitedAdminPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
And the class:
class LimitedAdminPermissions < Ingress::Permissions
inherits AdminPermissions
do
cannot :destroy, Comment
end
end
So basically, we can inherit permissions that are defined in other classes, and either switch off some or add others. Let's create
some sort of super_member
role, which can do everything a member can do, but can also update anything in the system:
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
:member, MemberPermissions
:admin, AdminPermissions
:limited_admin, LimitedAdminPermissions
:super_member, SuperMemberPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
And the class:
class SuperMemberPermissions < Ingress::Permissions
inherits MemberPermissions
do
can :update, "*"
end
end
We can inherit permissions, and we use a wildcard subject, to allow a user with the super_member
role to be able to update anything. We
can even define a common set of permissions which we want multiple roles to share and have the permission class for each of those roles
inherit from the common set. Let's say we want a financial_officer
role and a reporting_officer
role both of which should have the ability to do anything with a Transaction
object in our system (for whatever reason):
class UserPermissions < Ingress::Permissions
:guest, GuestPermissions
:member, MemberPermissions
:admin, AdminPermissions
:limited_admin, LimitedAdminPermissions
:super_member, SuperMemberPermissions
:financial_officer, FinancialOfficerPermissions
:reporting_officer, ReportingOfficerPermissions
def user_role_identifiers
user.roles.map do |role|
role.name.to_sym
end
end
end
And the classes:
class CommonPermissions < Ingress::Permissions
do
can "*", Transaction
end
end
class FinancialOfficerPermissions < Ingress::Permissions
inherits CommonPermissions
end
class ReportingOfficerPermissions < Ingress::Permissions
inherits CommonPermissions
end
Now we wildcard the action, so we can do anything to Transaction
objects. And we have to other sets of permission inherit from
the CommonPermissions
class.
I hope it's relatively clear that it's pretty flexible, you can almost endlessly decompose the permission definitions into smaller classes
then combine via inherits
and assign the final permission set to a role identifier via define_role_permissions
on the main
UserPermissions
class.
So now the authorization in your system can be defined in a much more OO way, without nasty and complex tricks. And you can still enjoy a nice syntax very similar to CanCan.
This framework has no hooks into Rails (these would be trivial to write if necessary, e.g. you can instantiate the user_permissions
object on your ApplicationController
and then do the can?
checks anywhere you want) and can therefore be used with any web framework, or even outside of the context of a web framework (if such a use case makes sense).
Conditional Lambda
Ingress by default does not know about the subject that is given in the conditional lambda. The given subject can be a Class or an Object and it depends on the user to define the correct lambda to handle the given subject.
For example, given the following UserPermissions
:
class UserPermissions < Ingress::Permissions
define_role_permissions do
can :read, Post, if: ->(user, given_subject) do
case [user, given_subject]
in [_, Post]
user.id == given_subject.user_id
in [_, Class]
true
else
false
end
end
end
This is the result of the defined permissions:
= UserPermissions.new(user)
.can?(:read, Post) # returns true
post = user.posts.first # assume we can get the list of posts form the user object
.can?(:read, post) # returns true
Ingress provides 2 convenient interfaces to apply conditional lambda on a Class or an Instance:
- if_subject_is_an_instance
- if_subject_is_a_class
These conditional lambdas always take 3 parameters: the user
, the subject
(this is either a Class or an Instance), and the options
(this is additional attributes that may be needed to do the check).
They can be chained together like following:
class UserPermissions < Ingress::Permissions
do
can :read, Post,
if: ->(user, _post) { !user.id.nil? },
if_subject_is_an_instance: ->(user, post, ) { user.id == post.user_id },
if_subject_is_a_class: ->(_user, klass, ) { klass == Post }
end
end
This is the result of the defined permissions:
= UserPermissions.new(user)
.can?(:read, Post) # returns true
post = user.posts.first # assume we can get the list of posts form the user object
.can?(:read, post) # returns true
Development
After checking out the repo, run script/setup
to install dependencies. Then, run rake spec
to run the tests. You can also run script/console
for an interactive prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install
. To release a new version, update the version number in version.rb
, and then run bundle exec rake release
, which will create a git tag for the version, push git commits and tags, and push the .gem
file to rubygems.org.
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/skorks/ingress.
License
The gem is available as open source under the terms of the MIT License.