Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Added grant_attributes functionality #2

Open
wants to merge 20 commits into from

3 participants

@maxjustus

I've added a Grant::ModelAttributeSecurity module which allows you to grant permission to change specific fields in the database. It uses ActiveRecord's changed instance method to determine what attributes have been updated. Let me know what you guys think.

@kunklejr
Owner

The attribute security is a nice addition. I hope to get some more time in the coming days to review all your changes and incorporate them. I appreciate you adding to and updating the specs too.

Curiously, what made you decide to add a new method (grant_attributes) instead of adding to the API through the grant method?

@maxjustus

The reason I did it was to keep things modular, since there might be instances where users only need security on methods, and others where they only need security on attributes. If I were to rework it to expand on the existing api would something like this be what you had in mind?

grant(:actions => [:find, :update]) {|user, model| user.thing == stuff}
grant(:attributes => [:user_id, :fun]) { true }
grant(:attributes => [:user_id, :fun], :actions => [:create]) { |user, model| user.admin? }
@kunklejr
Owner

How confusing do you think it would be to do a combination of all these ideas? For example

grant(:find, :update, :attr_1, :attr_2) {|user, model| user.thing == 'stuff'}
grant(actions => [:find, :update], :attributes => [:fun]) {|user, model| user.thing == 'stuff'}
grant(:find, :update, :attributes => [:create, :destroy]) {|user, model| user.thing == 'stuff'}

In essence, you'd just mix the action names with the attribute names and only use the :actions or :attributes hash parameters when there's a conflict. For example, if you wanted to restrict setting a model attribute named update but didn't want to restrict updating records as a whole, you'd use :attributes => [:update] to specify it.

Thinking about it a bit more, there's probably no need to have the actions parameter if everything is mixed together, unless you want to use it to better communicate your intent. Simply specifying conflicting attributes with the :attributes hash parameter would do the trick.

The main benefit I can see to doing this is being able to share the logic used to allow or deny between actions and attributes rather than having to duplicate the code in grant and grant_attributes if they happen to require the same logic.

What do you think?

@maxjustus maxjustus merge grant_attributes into grant method, change granted_attributes t…
…o granted and granted_attributes? to granted? Update readme to reflect changed api
9f31a55
@maxjustus

I like it! I pushed the changes to make it work that way and updated the specs. I also changed granted_attributes and granted_attributes? to be granted and granted? so they work for actions as well as attributes.

@kunklejr
Owner

Thanks for your excellent work. I did find a couple of things that don't run under Ruby 1.8.7 and plan to address those before merging the changes. I have a few projects that haven't yet been upgraded to 1.9.x.

@maxjustus

No problem! I didn't realize my latest changes had broken ruby 1.8.7 compatibility, so I went ahead and fixed the problems. I also tested it with JRuby and Rubinius and all the specs pass.

@kunklejr
Owner

I apologize for not merging these changes in yet. I have a current project that uses Grant heavily and the new changes have caused several tests to fail. I'll get back to you after I sort out the reasons.

@ihmccreery

I want to know what's going on with this. I would love to see this capability (and others) in Grant.

@kunklejr
Owner

The implementation has changed quite a bit since your contribution. I'd have to get back into it to see if it still works.

Curiously, what else would you like to see in Grant?

@ihmccreery

I've started a new thread to discuss other features to add.

@ihmccreery

I'm still interested in implementing the features that maxjustus implemented, and rolling them into Grant. I've forked Grant to myself and will begin working on it soon, likely referencing what maxjustus has done. Is anyone else interested in working on this project?

@kunklejr, if I implement column-level security in Grant, would you be willing to take a pull request?

@kunklejr
Owner

I would be willing assuming there's some testing for the new functionality. I can't in good conscience merge something that doesn't have associated tests.

Having said that, I'm more than happy to discuss the solution with you, so please check back as you go if there's anything worth discussing. Thanks!

@ihmccreery

Looking back through this issue, it looks like there were some problems with backward compatibility. How would you feel about bumping to a new version and ridding ourselves of that concern (making Grant only compatible with Ruby 1.9 and above)? Do you still have projects that are working in Ruby 1.8?

@ihmccreery

Also, I should note that I've never worked on this kind of open-source project before, so it may be a little bumpy. You'll have to bear with me while I figure out how this stuff is best done. I'm definitely always interested in suggestions on how to do things differently, what to do next, etc. I'm here to get stuff done, but also very much to learn.

@kunklejr
Owner

No problem. The best thing to do is start from the latest code in the master branch, rather than working from this pull request. It will be much easier to merge back in.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Feb 5, 2011
  1. @maxjustus
  2. @maxjustus

    update readme

    maxjustus authored
  3. @maxjustus
  4. @maxjustus

    make readme more consistent

    maxjustus authored
  5. @maxjustus

    remove unneccesary require

    maxjustus authored
Commits on Feb 7, 2011
  1. @maxjustus
  2. @maxjustus
  3. @maxjustus
  4. @maxjustus
Commits on Feb 8, 2011
  1. @maxjustus

    ruby 1.8.7 compatability

    maxjustus authored
Commits on Feb 14, 2011
  1. @maxjustus

    merge grant_attributes into grant method, change granted_attributes t…

    maxjustus authored
    …o granted and granted_attributes? to granted? Update readme to reflect changed api
  2. @maxjustus

    slight refactor

    maxjustus authored
  3. @maxjustus

    more refactoring

    maxjustus authored
Commits on Feb 17, 2011
  1. @maxjustus
Commits on Feb 18, 2011
  1. @maxjustus
  2. @maxjustus
  3. @maxjustus
Commits on Feb 22, 2011
  1. @maxjustus
Commits on Feb 24, 2011
  1. @maxjustus
  2. @maxjustus
This page is out of date. Refresh to see the latest.
View
1  .gitignore
@@ -1,3 +1,4 @@
coverage
rdoc
grant-*.gem
+*.swp
View
75 README.rdoc
@@ -40,21 +40,88 @@ Grant needs to know who the current user is, but with no standard for doing so y
= Usage
-To enable model security you simply include the Grant::ModelSecurity module in your model class. In the example below you see two grant statements. The first grants find (aka read) permission all the time. The second example grants create, update, and destroy permission when the passed block evaluates to true, which in this case happens when the model is editable by the current user. A Grant::Error is raised if any grant block evaluates to false or nil.
+To enable model security you simply include the Grant::ModelSecurity module in your model class. In the example below you see two grant statements. The first grants find (aka read) permission all the time, and permission to update every attribute all the time. The second example grants create, update, and destroy permission when the passed block evaluates to true, which in this case happens when the model is editable by the current user. A Grant::Error is raised if any grant block evaluates to false or nil.
class Book < ActiveRecord::Base
include Grant::ModelSecurity
has_many :tags
- grant(:find) { true }
+ grant(:find, :attributes => :all) { true }
grant(:create, :update, :destroy) { |user, model| model.editable_by_user? user }
- def editable_by_user? user
+ def editable_by_user?(user)
user.administrator? || user.has_role?(:editor)
end
end
-The valid actions to pass to a grant statement are :find, :create, :update, and :destroy. Each action can be passed as a Symbol or String. Any number of actions can be passed to a single grant statement, which is very useful if each of the actions share the same logic for determining access.
+The valid actions to pass to a grant statement are :find, :create, :update, and :destroy. The valid attributes to pass to a grant statement are those which correspond to columns in the database. Each action and attribute can be passed as a Symbol or String. Any number and combination of actions and model attributes can be passed to a single grant statement, which is very useful if each of the actions and attributes share the same logic for determining access.
+
+You can also use the grant method to specify granular permissions for the user to change attributes on a model. The example below grants any user the ability to find and create, but only owners and administrators to update or destroy. It also grants any user the ability to update user_id when the book is a new record, but only administrators if it is not new. It grants anyone the ability to update :title and :price if the record is new or they own it, but it only allows administrators to update the list_in_store attribute. A Grant::Error is raised if any grant block evaluates to false or nil.
+
+ class Book < ActiveRecord::Base
+ include Grant::ModelSecurity
+
+ has_many :tags
+ grant(:find, :create) { true }
+ grant(:update, :destroy) { |user, model| model.owned_by_user? user || user.administrator? }
+
+ grant(:title, :price) { |user, model| model.new_record? || model.owned_by_user? user }
+ grant(:user_id) { |user, model| model.new_record? || user.administrator? }
+ grant(:list_in_store) { |user, model| user.administrator? }
+
+ def owned_by_user?(user)
+ user.administrator? || user.owns?(self)
+ end
+ end
+
+If you happen to have a column name called find, create, update, or destroy you can explicitly define permissions on those attributes using the :attributes argument. This example grants permission to use the find and create actions, and to change the name, find, and create attributes
+
+ class Book < ActiveRecord::Base
+ grant(:find, :create, :attributes => [:name, :find, :create]) { true }
+ end
+
+In the event that you just want to grant permission to update all attributes you can use the argument :attributes => :all
+ class Book < ActiveRecord::Base
+ grant(:find, :create, :attributes => :all) { true }
+ end
+
+You can see what attributes and actions are granted for the current user to edit by using model.granted. This is useful in situations where you're using something like formtastic and want to make the form display only fields the user is granted to edit. You can use grantedwithout arguments to get all granted attributes and actions, or you can pass it a list of attributes and actions to check against, in which case it will return a hash of granted attributes and actions in the order of and limited to those passed. You can also pass :granted => false to get all ungranted attributes and actions, or a subset based on those passed in:
+
+ <%= semantic_form_for book do |f| %>
+ <%= f.inputs *book.granted(:title, :list_in_store, :price)[:attributes] %>
+
+ <% ungranted = book.granted(:title, :list_in_store, :price, :granted => false) %>
+ <% if ungranted.all? {|k,v| v.length > 0} %>
+ You are not allowed to edit
+ <%= ungranted[:attributes].join(', ') %>
+
+ and are not permitted to
+ <%= ungranted[:actions].join(', ') %>
+ <% end %>
+ <% end %>
+
+You can test whether a set of attributes and or actions are granted for the current user by using model.granted? with a list of attributes and actions to test against:
+
+ <% if book.granted?(:destroy, :title) %>
+ You can destroy this book and edit the title!
+ <% end %>
+
+ <% if book.granted?(:title) %>
+ You can edit the title!
+ %p
+ f.label :title
+ f.check_box :title
+ <% end %>
+
+ <% if book.granted?(:list_in_store, :price) %>
+ You can edit the price and whether this book is listed in the store!
+ %p
+ f.label :list_in_store
+ f.check_box :list_in_store
+ %p
+ f.label :price
+ f.text_field :price
+ <% end %>
= Integration
View
2  lib/grant.rb
@@ -5,4 +5,4 @@
module Grant
class Error < StandardError; end
-end
+end
View
57 lib/grant/config_parser.rb
@@ -1,22 +1,61 @@
module Grant
class ConfigParser
- def self.extract_config(args)
- normalize_config args
- validate_config args
+ def self.extract_config(args, resource)
+ args = normalize_config args, resource
+ validate_config args, resource
args
end
private
- def self.normalize_config(actions)
- actions.each_with_index { |item, index| actions[index] = item.to_sym unless item.kind_of? Symbol }
+ def self.normalize_config(args, resource)
+ normalized_args = {:actions => [], :attributes => []}
+ args.each_with_index do |item, index|
+ if item.kind_of? Hash
+ attrs = item[:attributes] ? item[:attributes] : item['attributes']
+
+ if !attrs.kind_of?(Array) && attrs.to_sym == :all
+ if resource.table_exists?
+ attrs = resource.column_names.collect {|c| c.to_sym}
+ else
+ attrs = []
+ end
+ end
+
+ normalized_args[:attributes] << attrs
+ else
+ item = item.to_sym
+
+ if [:create, :find, :update, :destroy].include?(item)
+ normalized_args[:actions] << item
+ else
+ normalized_args[:attributes] << item
+ end
+ end
+ end
+
+ normalized_args[:attributes] = normalized_args[:attributes].flatten.uniq
+ normalized_args
end
- def self.validate_config(actions)
- raise Grant::Error.new("at least one :create, :find, :update, or :destroy action must be specified") if actions.empty?
- raise Grant::Error.new(":create, :find, :update, and :destroy are the only valid actions") unless actions.all? { |a| [:create, :find, :update, :destroy].include? a }
+ def self.validate_config(args, resource)
+ if args[:actions] == nil && args[:attributes] == nil
+ valid_args = [:create, :find, :update, :destroy]
+ resource.column_names.each do |attr|
+ valid_args << attr
+ end
+
+ raise Grant::Error.new("at least one of " + valid_args.join(', ') + " must be specified")
+ end
+
+ raise Grant::Error.new(":create, :find, :update, and :destroy are the only valid actions") unless args[:actions].all? { |a| [:create, :find, :update, :destroy].include? a }
+
+ if resource.table_exists?
+ attribute_names = resource.column_names
+ raise Grant::Error.new(attribute_names.join(', ') + " are the only valid attributes") unless args[:attributes].all? { |a| attribute_names.include? a.to_s }
+ end
end
end
-end
+end
View
141 lib/grant/model_security.rb
@@ -4,9 +4,114 @@
module Grant
module ModelSecurity
+ def grant_current_user
+ Grant::User.current_user
+ end
+
+ def grant_disabled?
+ Grant::ThreadStatus.disabled? || @grant_disabled
+ end
+
+ def grant_raise_error(user, action, model, association_id=nil)
+ user_name = user != nil ? "#{user.class.name}:#{user.id}" : "unlogged in user"
+ model_name = model.id ? "#{model.class.name}:#{model.id}" : "new #{model.class.name}"
+ msg = ["#{action} permission",
+ "not granted to #{user_name}",
+ "for resource #{model_name}"]
+
+ raise Grant::Error.new(msg.join(' '))
+ end
+
+ def granted(*args)
+ if args.last.instance_of?(Hash)
+ options = args.pop
+ options = Hash[options.collect {|option,value| [option.to_sym, value]}]
+ else
+ options = {:granted => true}
+ end
+
+ args = Grant::ConfigParser.extract_config(args, self.class)
+ granted_attrs_and_actions = {:actions => [], :attributes => []}
+ no_arguments = args.all? {|k,v| v.length == 0}
+
+ args.each do |arg_type,arg_list|
+ #if arguments exist for the current key, or no arguments exist
+ if arg_list.length > 0 || no_arguments
+ #get all permissions and filter by options[:granted]
+ granted = eval("granted_#{arg_type}(args[arg_type])").select {|a,granted| granted == options[:granted]}.collect {|a,granted| a}
+ granted.sort! {|v1,v2| v1.to_s <=> v2.to_s} if no_arguments
+
+ granted_attrs_and_actions[arg_type] = granted
+ end
+ end
+
+ granted_attrs_and_actions
+ end
+
+ def granted?(*args)
+ granted(*args.push(:granted => false)).all? {|k, v| v.length == 0}
+ end
+
+ private
+
+ def granted_actions(check_actions)
+ check_actions = check_actions.length == 0 ? [:create, :update, :destroy, :find] : check_actions
+
+ #make a hash of action names with their grant status
+ grant_actions = Hash[*check_actions.collect {|action| [action.to_sym, false]}.flatten]
+
+ grant_actions.each_key do |action|
+ callback = (action == :find ? "after_#{action}" : "before_#{action}")
+ begin
+ eval "grant_#{callback}"
+ grant_actions[action] = true
+ rescue
+ grant_actions[action] = false
+ end
+ end
+
+ grant_actions
+ end
+
+ def granted_attributes(check_attributes)
+ check_attributes = check_attributes.length == 0 ? self.attribute_names : check_attributes
+
+ #make a hash of attribute names with their grant status
+ grant_attributes = check_attributes.collect {|attr| [attr.to_sym, false]}
+
+ if grant_disabled?
+ grant_attributes.each_with_index do |attr, index|
+ attr[1] = true
+ grant_attributes[index] = attr
+ end
+ else
+ self.class.granted_permissions.each do |attrs_and_blk|
+ attrs = attrs_and_blk[0].select {|a| grant_attributes.assoc(a)}
+ if attrs.length > 0
+ blk = attrs_and_blk[1]
+ blk_result = !!blk.call(grant_current_user, self)
+ attrs.each do |attr|
+ grant_attributes.each_with_index do |grant_attr, index|
+ if grant_attr[0] == attr.to_sym
+ grant_attr[1] = blk_result
+ grant_attributes[index] = grant_attr
+ end
+ end
+ end
+ end
+ end
+ end
+
+ grant_attributes
+ end
def self.included(base)
- [:create, :update, :destroy, :find].each do |action|
+ base.class_eval do
+ @granted_permissions = []
+ class << self; attr_accessor :granted_permissions; end
+ end
+
+ [:create, :update, :destroy, :find, :save].each do |action|
callback = (action == :find ? "after_#{action}" : "before_#{action}")
base.class_eval <<-RUBY
def grant_#{callback}
@@ -22,31 +127,29 @@ def grant_#{callback}
# ActiveRecord won't call the after_find handler unless it see's a specific after_find method defined
def after_find; end
- def grant_current_user
- Grant::User.current_user
- end
-
- def grant_disabled?
- Grant::ThreadStatus.disabled? || @grant_disabled
- end
-
- def grant_raise_error(user, action, model, association_id=nil)
- msg = ["#{action} permission",
- "not granted to #{user.class.name}:#{user.id}",
- "for resource #{model.class.name}:#{model.id}"]
-
- raise Grant::Error.new(msg.join(' '))
- end
-
module ClassMethods
def grant(*args, &blk)
- actions = Grant::ConfigParser.extract_config(args)
- actions.each do |action|
+ args = Grant::ConfigParser.extract_config(args, self)
+
+ args[:actions].each do |action|
grant_callback = (action.to_sym == :find ? "grant_after_find" : "grant_before_#{action}").to_sym
define_method(grant_callback) do
grant_raise_error(grant_current_user, action, self) unless grant_disabled? || blk.call(grant_current_user, self)
end
end
+
+ if args[:attributes]
+ @granted_permissions << [args[:attributes], blk]
+
+ define_method(:grant_before_save) do
+ if self.changed.length > 0 && !grant_disabled?
+ ungranted_changed = granted(*self.changed.push(:granted => false))[:attributes]
+ unless (ungranted_changed).length == 0
+ grant_raise_error(grant_current_user, ungranted_changed.join(', '), self)
+ end
+ end
+ end
+ end
end
end
View
92 spec/config_parser_spec.rb
@@ -2,33 +2,103 @@
require 'grant'
describe Grant::ConfigParser do
+ before do
+ @resource = ActiveRecordMock
+ end
describe 'Configuration' do
it "should parse actions from a config array" do
- config = Grant::ConfigParser.extract_config([:create, 'update'])
- config.should_not be_nil
- config.should =~ [:create, :update]
+ config = Grant::ConfigParser.extract_config([:create, 'update'], @resource)
+ config[:actions].should =~ [:create, :update]
end
it "should parse actions" do
- config = Grant::ConfigParser.extract_config([:create])
- config.should_not be_nil
- config.should =~ [:create]
+ config = Grant::ConfigParser.extract_config([:create], @resource)
+ config[:actions].should =~ [:create]
+ end
+
+ it "should parse attributes" do
+ config = Grant::ConfigParser.extract_config([:name], @resource)
+ config[:attributes].should =~ [:name]
+ end
+
+ it "should parse arguments as strings" do
+ config = Grant::ConfigParser.extract_config(['create', 'name'], @resource)
+ config[:attributes].should =~ [:name]
+ config[:actions].should =~ [:create]
+ end
+
+ it "should parse attributes with actions" do
+ config = Grant::ConfigParser.extract_config([:create, :name], @resource)
+ config[:attributes].should =~ [:name]
+ config[:actions].should =~ [:create]
+ end
+
+ it "should parse actions with attributes specified using :attributes" do
+ config = Grant::ConfigParser.extract_config([:create, {:attributes => [:name, :stuff]}], @resource)
+ config[:attributes].should =~ [:name, :stuff]
+ config[:actions].should =~ [:create]
+ end
+
+ it "should parse actions with attributes specified using :attributes and as normal arguments" do
+ config = Grant::ConfigParser.extract_config([:create, :stuff, {:attributes => [:create, :stuff]}], @resource)
+ config[:attributes].should =~ [:create, :stuff]
+ config[:actions].should =~ [:create]
+ end
+
+ it "should parse :attributes => :all and return every attribute" do
+ config = Grant::ConfigParser.extract_config([:create, {:attributes => :all}], @resource)
+ config[:attributes].should =~ @resource.column_names.collect {|c| c.to_sym}
+
+ config = Grant::ConfigParser.extract_config(['create', {'attributes' => 'all'}], @resource)
+ config[:actions].should =~ [:create]
+ config[:attributes].should =~ @resource.column_names.collect {|c| c.to_sym}
+ end
+
+ it "should parse actions with attributes specified using :attributes which are identical to action names" do
+ config = Grant::ConfigParser.extract_config([:create, :update, {:attributes => [:create]}], @resource)
+ config[:attributes].should =~ [:create]
+ config[:actions].should =~ [:create, :update]
+ end
+
+ it 'should not access model column_names method if table does not exist' do
+ ActiveRecordMock.should_receive(:table_exists?).at_least(:once).and_return(false)
+ ActiveRecordMock.should_not_receive(:column_names)
+ resource = @resource
+
+ Grant::ConfigParser.extract_config([:create, {:attributes => :all}], resource)
end
end
describe 'Configuration Validation' do
it "should raise a Grant::Error if no action is specified" do
+ resource = @resource
lambda {
- Grant::ConfigParser.instance_eval { validate_config([]) }
+ Grant::ConfigParser.instance_eval { validate_config({}, resource) }
}.should raise_error(Grant::Error)
end
-
+
it "should raise a Grant::Error if an invalid action is specified" do
+ resource = @resource
+ lambda {
+ Grant::ConfigParser.instance_eval { validate_config({:actions => [:create, :udate]}, resource) }
+ }.should raise_error(Grant::Error)
+ end
+
+ it "should raise a Grant::Error if an invalid attribute is specified" do
+ resource = @resource
lambda {
- Grant::ConfigParser.instance_eval { validate_config([:create, :udate]) }
+ Grant::ConfigParser.instance_eval { validate_config({:actions => [:create], :attributes => [:guy, :stuff]}, resource) }
}.should raise_error(Grant::Error)
end
+
+ it "should not raise a Grant::Error if the table does not exist" do
+ ActiveRecordMock.should_receive(:table_exists?).at_least(:once).and_return(false)
+ resource = @resource
+
+ lambda {
+ Grant::ConfigParser.instance_eval { validate_config({:actions => [:create], :attributes => [:guy, :stuff]}, resource) }
+ }.should_not raise_error(Grant::Error)
+ end
end
-
-end
+end
View
54 spec/mocks/active_record_mock.rb
@@ -0,0 +1,54 @@
+class ActiveRecordMock
+ def self.table_exists?
+ true
+ end
+
+ def self.column_names
+ ['name', 'stuff', 'other_attr', 'ungranted_attr', 'create']
+ end
+
+ def id; 1 end
+
+ def name
+ 'thing'
+ end
+
+ def attribute_names
+ ['name', 'stuff', 'other_attr', 'ungranted_attr', 'create']
+ end
+
+ def changed
+ ['name', 'stuff']
+ end
+
+ def self.before_save(method)
+ alias_method :before_save_create, :create
+ alias_method :before_save_update, :update
+ define_method(:create) { send :before_save_create; send method }
+ define_method(:update) { send :before_save_update; send method }
+ end
+
+ def create
+ end
+
+ def update
+ end
+
+ def self.before_create(method)
+ alias_method :orig_create, :create
+ define_method(:create) { send :orig_create; send method }
+ end
+
+ def self.before_update(method)
+ alias_method :orig_update, :update
+ define_method(:update) { send :orig_update; send method }
+ end
+
+ def self.before_destroy(method)
+ define_method(:destroy) { send method }
+ end
+
+ def self.after_find(method)
+ define_method(:find) { send method }
+ end
+end
View
294 spec/model_security_spec.rb
@@ -6,15 +6,16 @@
before(:each) do
Grant::User.current_user = (Class.new do
def id; 1 end
+ def auth_level; 10 end
end).new
end
describe 'module include' do
- it 'should establish failing ActiveRecord callbacks for before_create, before_update, before_destroy, and after_find when included' do
+ it 'should establish failing ActiveRecord callbacks for before_save, before_create, before_update, before_destroy, and after_find when included' do
verify_standard_callbacks(new_model_class.new)
end
end
-
+
describe '#grant' do
it 'should allow after_find callback to succeed when granted' do
c = new_model_class.instance_eval { grant(:find) { true }; self }
@@ -22,12 +23,18 @@ def id; 1 end
end
it 'should allow before_create callback to succeed when granted' do
- c = new_model_class.instance_eval { grant(:create) { true }; self }
+ c = new_model_class.instance_eval do
+ grant(:create, :attributes => :all) { true }
+ self
+ end
verify_standard_callbacks(c.new, :create)
end
it 'should allow before_update callback to succeed when granted' do
- c = new_model_class.instance_eval { grant(:update) { true }; self }
+ c = new_model_class.instance_eval do
+ grant(:update, :attributes => [:name, :stuff]) { true }
+ self
+ end
verify_standard_callbacks(c.new, :update)
end
@@ -35,17 +42,264 @@ def id; 1 end
c = new_model_class.instance_eval { grant(:destroy) { true }; self }
verify_standard_callbacks(c.new, :destroy)
end
+
+ it 'should raise a grant error for unsaved models' do
+ c = new_model_class.class_eval do
+ def id
+ nil
+ end
+
+ self
+ end
+
+ c.instance_eval do
+ grant(:create) {false}
+ end
+
+ lambda { c.new.send(:create) }.should(raise_error(Grant::Error, 'create permission not granted to :1 for resource new '))
+ end
+
+ it 'should raise a grant error for nil users' do
+ Grant::User.current_user = nil
+ c = new_model_class
+
+ lambda { c.new.send(:create) }.should(raise_error(Grant::Error, 'create permission not granted to unlogged in user for resource :1'))
+ end
- it 'should allow multiple callbacks to be specified with one grant statment' do
- c = new_model_class.instance_eval { grant(:create, :update) { true }; self }
+ it 'should allow multiple callbacks to be specified with one grant statement' do
+ c = new_model_class.instance_eval { grant(:create, :update, :attributes => :all) { true }; self }
verify_standard_callbacks(c.new, :create, :update)
- c = new_model_class.instance_eval { grant(:create, :update, :destroy) { true }; self }
+ c = new_model_class.instance_eval { grant(:create, :update, :destroy, :attributes => :all) { true }; self }
verify_standard_callbacks(c.new, :create, :update, :destroy)
- c = new_model_class.instance_eval { grant(:create, :update, :destroy, :find) { true }; self }
+ c = new_model_class.instance_eval { grant(:create, :update, :destroy, :find, :attributes => :all) { true }; self }
verify_standard_callbacks(c.new, :create, :update, :destroy, :find)
end
+
+ it 'should allow update of attributes specified using grant' do
+ c = new_model_class.instance_eval do
+ grant(:create, :update, :name) { true }
+ grant(:stuff, :other_attr) { true }
+ self
+ end
+ verify_standard_callbacks(c.new, :create, :update)
+ end
+
+ it 'should pass current_user and model to block' do
+ c = new_model_class.instance_eval do
+ grant(:create, :update, :name, :stuff, :other_attr) {|user, model| user.auth_level == 10 && model.name == 'thing' }
+ self
+ end
+
+ verify_standard_callbacks(c.new, :create, :update)
+ end
+
+ it 'should be restrictive rather then permissive' do
+ c = new_model_class.instance_eval do
+ grant(:name) { true }
+ self
+ end
+ verify_standard_callbacks(c.new)
+ end
+
+ it 'should respect grant_disabled' do
+ c = new_model_class.instance_eval do
+ grant(:name) { false }
+ grant(:find) { false }
+ self
+ end
+
+ c.class_eval do
+ def grant_disabled?
+ true
+ end
+ end
+
+ verify_standard_callbacks(c.new, :create, :update, :find, :destroy)
+
+ c = new_model_class
+
+ c.class_eval do
+ def grant_disabled?
+ true
+ end
+ end
+
+ verify_standard_callbacks(c.new, :create, :update, :find, :destroy)
+ end
+
+ it 'should allow update if nothing is changed' do
+ c = new_model_class.instance_eval do
+ grant(:name) { false }
+ grant(:update, :create, :stuff, :other_attr) { true }
+
+ self
+ end
+ @c = c.new
+ @c.instance_eval do
+ def changed
+ []
+ end
+ end
+
+ verify_standard_callbacks(@c, :create, :update)
+ end
+
+ it 'should deny update of attributes where grant user may not update a changed attribute' do
+ c = new_model_class.instance_eval do
+ grant(:name) { false }
+ grant(:stuff, :other_attr) { true }
+ self
+ end
+
+ verify_standard_callbacks(c.new)
+ end
+
+ it 'should deny update of attributes when nil is used as the return value for block' do
+ c = new_model_class.instance_eval do
+ grant(:name, :stuff, :other_attr) { true }
+ grant(:name) { nil }
+ self
+ end
+
+ verify_standard_callbacks(c.new)
+ end
+
+ it 'should allow multiple attributes to be specified with one grant statement' do
+ c = new_model_class.instance_eval do
+ grant(:create, :update) { true }
+ grant(:name, :stuff, :other_attr) { true }
+ self
+ end
+ verify_standard_callbacks(c.new, :create, :update)
+
+ c = new_model_class.instance_eval do
+ grant(:name, :stuff) { false }
+ grant(:other_attr) { true }
+ self
+ end
+ verify_standard_callbacks(c.new)
+ end
+ end
+
+ describe '#granted' do
+ before do
+ c = new_model_class.instance_eval do
+ grant(:create) { true }
+ grant(:stuff, :other_attr) { true }
+ grant(:name) { false }
+ self
+ end
+ @c = c.new
+ end
+
+ it 'should return a hash of attributes and actions' do
+ @c.granted[:attributes].should_not be_nil
+ @c.granted[:actions].should_not be_nil
+ end
+
+ it 'should sort returned attributes alphabetically when no attributes are passed in to filter by' do
+ @c.granted[:attributes].should == [:other_attr, :stuff]
+ end
+
+ it 'should list granted attributes for current_user' do
+ @c.granted[:attributes].should =~ [:other_attr, :stuff]
+ end
+
+ it 'should list granted attributes and actions for current_user when passed :granted => true' do
+ @c.granted(:granted => true)[:attributes].should =~ [:other_attr, :stuff]
+ @c.granted(:granted => true)[:actions].should == [:create]
+ end
+
+ it 'should list ungranted actions and attributes for current_user when passed false' do
+ @c.granted(:granted => false)[:attributes].should =~ [:create, :name, :ungranted_attr]
+ @c.granted(:granted => false)[:actions].should =~ [:find, :update, :destroy]
+ end
+
+ it 'should recognize arguments as strings' do
+ @c.granted('granted' => false)[:attributes].should =~ [:create, :name, :ungranted_attr]
+ end
+
+ context 'given a list of attributes or actions' do
+ it 'should return a limited list of attributes or actions that are granted' do
+ @c.granted(:stuff, :other_attr, :granted => true)[:attributes].should == [:stuff, :other_attr]
+ @c.granted(:name, :granted => true)[:attributes].should == []
+ @c.granted(:name, :granted => true)[:actions].should == []
+ @c.granted(:name, :find, :create, :granted => true)[:actions].should == [:create]
+ end
+
+ it 'should return a limited list of attributes or actions that are not granted' do
+ @c.granted(:name, :granted => false)[:attributes].should == [:name]
+ @c.granted(:name, :find, :granted => false)[:actions].should == [:find]
+ end
+
+ it 'should return list of attributes in order it was passed in' do
+ @c.granted(:other_attr, :stuff)[:attributes].should == [:other_attr, :stuff]
+ @c.granted(:stuff, :other_attr)[:attributes].should == [:stuff, :other_attr]
+ end
+
+ it 'should recognize attributes passed in as strings' do
+ @c.granted('stuff', 'other_attr', :granted => true)[:attributes].should == [:stuff, :other_attr]
+ end
+ end
+
+ context 'grant_disabled' do
+ before do
+ c = new_model_class.instance_eval do
+ grant(:stuff, :other_attr) { true }
+ grant(:name, :find, :create) { false }
+ self
+ end
+
+ c.class_eval do
+ def grant_disabled?
+ true
+ end
+ end
+
+ @c = c.new
+ end
+
+ it 'should list all attributes and actions as granted' do
+ @c.granted(:granted => true)[:attributes].should =~ [:name, :other_attr, :stuff, :ungranted_attr, :create]
+ @c.granted(:granted => true)[:actions].should =~ [:create, :find, :update, :destroy]
+ @c.granted(:name, :granted => true)[:attributes].should == [:name]
+ end
+
+ it 'should list no attributes as ungranted' do
+ @c.granted(:granted => false).should == {:attributes => [], :actions => []}
+ @c.granted(:name, :granted => false).should == {:attributes => [], :actions => []}
+ end
+ end
+ end
+
+ describe '#granted?' do
+ before do
+ c = new_model_class.instance_eval do
+ grant(:stuff, :other_attr, :find) { true }
+ grant(:name) { false }
+ self
+ end
+ @c = c.new
+ end
+
+ it 'should return true if user is granted permission for passed in attribute or action' do
+ @c.granted?(:stuff).should == true
+ @c.granted?(:name).should == false
+ @c.granted?(:update).should == false
+ @c.granted?(:find).should == true
+ end
+
+ context 'multiple arguments' do
+ it 'should return false when one of the attributes or actions passed in is not granted' do
+ @c.granted?(:name, :stuff, :find).should == false
+ end
+
+ it 'should return true when all of the attributes and actions passed in are granted' do
+ @c.granted?(:stuff, :other_attr, :find).should == true
+ end
+ end
end
def verify_standard_callbacks(instance, *succeeding_callbacks)
@@ -55,7 +309,7 @@ def verify_standard_callbacks(instance, *succeeding_callbacks)
def verify_callbacks(all_actions, instance, associated_model, succeeding_callbacks)
all_actions.each do |action|
expectation = succeeding_callbacks.include?(action) ? :should_not : :should
- lambda { instance.send(action, associated_model) }.send(expectation, raise_error(Grant::Error))
+ lambda { instance.send(action) }.send(expectation, raise_error(Grant::Error))
end
end
@@ -64,25 +318,5 @@ def new_model_class
include Grant::ModelSecurity
end
end
-
- class ActiveRecordMock
- def id; 1 end
-
- def self.before_create(method)
- define_method(:create) { send method }
- end
-
- def self.before_update(method)
- define_method(:update) { send method }
- end
-
- def self.before_destroy(method)
- define_method(:destroy) { send method }
- end
-
- def self.after_find(method)
- define_method(:find) { send method }
- end
- end
-
+
end
View
1  spec/spec_helper.rb
@@ -3,6 +3,7 @@
# Requires supporting files with custom matchers and macros, etc,
# in ./support/ and its subdirectories.
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
+Dir["#{File.dirname(__FILE__)}/mocks/**/*.rb"].each {|f| require f}
RSpec.configure do |config|
# If you're not using ActiveRecord you should remove these
Something went wrong with that request. Please try again.