Skip to content

Instantly share code, notes, and snippets.

@tpett
Last active December 24, 2015 10:19
Show Gist options
  • Save tpett/6783496 to your computer and use it in GitHub Desktop.
Save tpett/6783496 to your computer and use it in GitHub Desktop.
An example of access level based authorization with CanCan.

This is an approach to handling tiered access levels within the CanCan gem. The user inherits access grants for all levels up to and including their granted access level. This implementation assumes a User class of some kind with a method called access_level that returns the appropriate AccessLevel instance for the current user. The ability.rb file contains an example of the DSL that the TieredAbility class implements. Specs are included.

Any feedback is appreciated!

class Ability < TieredAbility
at_level(:basic) do |user|
can :basic, Welcome
end
at_level(:contribute) do |user|
can :contribute, Welcome
end
at_level(:manage) do |user|
can :manage_action, Welcome
end
at_level(:appadmin) do |user|
can :appadmin, Welcome
end
at_level(:sysadmin) do |user|
can :manage, :all
end
end
ACCESS_LEVELS = [
{ id: nil, name: "None", slug: "none", description: "no access", level: 0 },
{ id: 1, name: "Basic", slug: "basic", description: "read access", level: 1 },
{ id: 2, name: "Contribute", slug: "contribute", description: "read-write of data user owns", level: 2 },
{ id: 3, name: "Manage", slug: "manage", description: "read-write of other's data", level: 3 },
{ id: 4, name: "App Admin", slug: "appadmin", description: "read-write app configuration", level: 4 },
{ id: 5, name: "System Admin", slug: "sysadmin", description: "full access", level: 5 },
]
AccessLevel = Struct.new(:id, :name, :slug, :description, :level) do
include Comparable
def <=>(record)
self.level <=> record.level
end
def self.[](index_or_slug, db=ACCESS_LEVELS)
all(db).select do |al|
al.level.to_s === index_or_slug.to_s ||
al.slug === index_or_slug.to_s
end.last
end
def self.all(db=ACCESS_LEVELS)
db.collect do |row|
new.tap { |access_level|
row.each { |field, value|
access_level[field] = value
}
}
end.sort
end
end
require 'cancan'
class TieredAbility
include CanCan::Ability
def initialize(user)
@user = user
grant_access_at(@user.access_level)
end
def grant_access_at(access_level)
levels.each do |level, can_blocks|
if access_level >= level
can_blocks.each { |can_block| instance_exec(@user, &can_block) }
end
end
end
def levels
self.class.levels
end
def self.at_level(level, &can_block)
levels[WCC::Auth::AccessLevel[level]] << can_block
end
def self.levels
@levels ||= Hash.new { |hash, key| hash[key] = [] }
end
def self.inherited(subclass)
subclass.send :instance_variable_set, :@levels, levels.dup
end
end
describe TieredAbility do
let(:klass) { TieredAbility }
let(:access_level) { AccessLevel }
describe "defining levels" do
it "stores blocks in ::levels" do
subclass = Class.new(klass) do
at_level(:basic) {}
at_level(:contribute) {}
at_level(:basic) {}
end
expect(subclass.levels[access_level[:basic]].size).to eq(2)
expect(subclass.levels[access_level[:contribute]].size).to eq(1)
end
end
describe "#initialize" do
it "grants access to the user's access_level" do
ability = Class.new(klass) do
at_level(:basic) { |user| user.ping }
end
user = double("User")
level = double("AccessLevel")
allow(level).to receive(:>=).and_return(true)
expect(user).to receive(:access_level).and_return(level)
expect(user).to receive(:ping)
ability.new(user)
end
end
describe "#grant_access_at" do
let(:mock_klass) {
Class.new(klass) do
def initialize(mock)
@user = mock
end
end
}
it "runs only the levels that the given access level allows" do
ability = Class.new(mock_klass) do
at_level(:basic) { |mock| mock.basic }
at_level(:appadmin) { |mock| mock.appadmin }
end
mock = double
expect(mock).to receive(:basic)
ability.new(mock).grant_access_at(access_level[:manage])
end
it "runs the blocks in the defined order" do
ability = Class.new(mock_klass) do
at_level(:appadmin) { |mock| mock.first }
at_level(:basic) { |mock| mock.second }
at_level(:manage) { |mock| mock.fourth }
at_level(:basic) { |mock| mock.third }
end
mock = double
expect(mock).to receive(:first).ordered
expect(mock).to receive(:second).ordered
expect(mock).to receive(:third).ordered
expect(mock).to receive(:fourth).ordered
ability.new(mock).grant_access_at(access_level[:sysadmin])
end
end
describe "subclassing" do
it "inherits defined levels from parent class" do
sub1 = Class.new(klass) do
at_level(:basic) {}
end
sub2 = Class.new(sub1)
expect(sub2.levels.size).to eq(1)
end
it "subclass and parent class have independent levels" do
sub1 = Class.new(klass) do
at_level(:basic) {}
end
sub2 = Class.new(sub1) do
at_level(:contribute) {}
end
expect(sub1.levels.size).to eq(1)
expect(sub2.levels.size).to eq(2)
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment