Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial commit of readme and canable functionality. Tests are next.

  • Loading branch information...
commit 71dbef59f8a397f084a3a58056721d96f8575a3d 1 parent c2a6637
@jnunemaker authored
Showing with 182 additions and 2 deletions.
  1. +107 −2 README.rdoc
  2. +75 −0 lib/canable.rb
View
109 README.rdoc
@@ -1,6 +1,111 @@
-= canable
+= Canable
-Description goes here.
+Simple permissions that I have used on my last several projects so I figured it was time to abstract and wrap up into something more easily reusable.
+
+== Cans
+
+Whatever class you want all permissions to run through should include Canable::Cans.
+
+ class User
+ include MongoMapper::Document
+ include Canable::Cans
+ end
+
+This means that an instance of a user automatically gets can methods for the default REST actions: can_view?(resource), can_create?(resource), can_update?(resource), can_destroy?(resource).
+
+== Ables
+
+Each of the can methods simply calls the related "able" method (viewable, creatable, updatable, destroyable) for the action (view, create, update, delete). Canable comes with defaults for this methods that you can then override as makes sense for your permissions.
+
+ class Article
+ include MongoMapper::Document
+ include Canable::Ables
+ end
+
+Including Canable::Ables adds the able methods to the class including it. In this instance, article now has viewable_by?(user), creatable_by?(user), updatable_by?(user) and destroyable_by?(user).
+
+Lets say an article can be viewed and created by anyone, but only updated or destroyed by the user that created the article. To do that, you could leave viewable_by? and creatable_by? alone as they default to true and just override the other methods.
+
+ class Article
+ include MongoMapper::Document
+ include Canable::Ables
+ userstamps! # adds creator and updater
+
+ def updatable_by?(user)
+ creator == user
+ end
+
+ def destroyable_by?(user)
+ updatable_by?(user)
+ end
+ end
+
+Lets look at some sample code now:
+
+ john = User.create(:name => 'John')
+ steve = User.create(:name =. 'Steve')
+
+ ruby = Article.new(:title => 'Ruby')
+ john.can_create?(ruby) # true
+ steve.can_create?(ruby) # true
+
+ ruby.creator = john
+ ruby.save
+
+ john.can_view?(ruby) # true
+ steve.can_view?(ruby) # true
+
+ john.can_update?(ruby) # true
+ steve.can_update?(ruby) # false
+
+ john.can_destroy?(ruby) # true
+ steve.can_destroy?(ruby) # false
+
+Now we can implement our permissions for each resource and then always check whether a user can or cannot do something. This makes it all really easy to test. Next, how would you use this in the controller.
+
+== Enforcers
+
+ class ApplicationController
+ include Canable::Enforcers
+ end
+
+Including Canable::Enforcers adds an enforce permission method for each of the actions defined (by default view/create/update/destroy). It is the same thing as doing this for each Canable action:
+
+ class ApplicationController
+ include Canable::Enforcers
+
+ delegate :can_view?, :to => :current_user
+ helper_method :can_view? # so you can use it in your views
+ hide_action :can_view?
+
+ private
+ def enforce_view_permission(resource)
+ raise Canable::Transgression unless can_view?(resource)
+ end
+ end
+
+Which means you can use it like this:
+
+ class ArticlesController < ApplicationController
+ def show
+ @article = Article.find!(params[:id])
+ enforce_view_permission(@article)
+ end
+ end
+
+If the user can_view? the article, all is well. If not, a Canable::Transgression is raised which you can decide how to handle (show 404, slap them on the wrist, etc.).
+
+== Adding Your Own Actions
+
+You can add your own actions like this:
+
+ Canable.add(:publish, :publishable)
+
+The first parameter is the can method (ie: can_publish?) and the second is the able method (ie: publishable_by?).
+
+== Review
+
+So, lets review: cans go on user model, ables go on everything, you override ables in each model where you want to enforce permissions, and enforcers go after each time you find or initialize an object in a controller. Bing, bang, boom.
== Note on Patches/Pull Requests
View
75 lib/canable.rb
@@ -0,0 +1,75 @@
+module Canable
+ # Module that holds all the can_action? methods.
+ module Cans; end
+
+ # Module that holds all the [method]able_by? methods.
+ module Ables; end
+
+ # Module that holds all the enforce_[action]_permission methods for use in controllers.
+ module Enforcers
+ def self.included(controller)
+ controller.class_eval do
+ Canable.actions.each do |can, able|
+ delegate "can_#{can}?", :to => :current_user
+ helper_method "can_#{can}?" if controller.respond_to?(:helper_method)
+ hide_action "can_#{can}?" if controller.respond_to?(:hide_action)
+ end
+ end
+ end
+ end
+
+ # Exception that gets raised when permissions are broken for whatever reason.
+ class Transgression < StandardError; end
+
+ # Default actions to an empty hash.
+ @actions = {}
+
+ # Returns hash of actions that have been added.
+ # {:view => :viewable, ...}
+ def self.actions
+ @actions
+ end
+
+ # Adds an action to actions and the correct methods to can and able modules.
+ #
+ # @param [Symbol] can_method The name of the can_[action]? method.
+ # @param [Symbol] resource_method The name of the [resource_method]_by? method.
+ def self.add(can, able)
+ @actions[can] = able
+ add_can_method(can, able)
+ add_able_method(able)
+ add_enforcer_method(can)
+ end
+
+ private
+ def self.add_can_method(can, able)
+ Cans.module_eval <<-EOM
+ def can_#{can}?(resource)
+ return false if resource.blank?
+ resource.#{able}_by?(self)
+ end
+ EOM
+ end
+
+ def self.add_able_method(able)
+ Ables.module_eval <<-EOM
+ def #{able}_by?(user)
+ true
+ end
+ EOM
+ end
+
+ def self.add_enforcer_method(can)
+ Enforcers.module_eval <<-EOM
+ def enforce_#{can}_permission(resource)
+ raise Canable::Transgression unless can_#{can}?(resource)
+ end
+ private :enforce_#{can}_permission
+ EOM
+ end
+end
+
+Canable.add(:view, :viewable)
+Canable.add(:create, :creatable)
+Canable.add(:update, :updatable)
+Canable.add(:destroy, :destroyable)
Please sign in to comment.
Something went wrong with that request. Please try again.