grant 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/README.rdoc +73 -0
- data/lib/grant.rb +8 -0
- data/lib/grant/config_parser.rb +26 -0
- data/lib/grant/integration.rb +49 -0
- data/lib/grant/model_security.rb +101 -0
- data/lib/grant/spec_helpers.rb +22 -0
- data/lib/grant/thread_local.rb +18 -0
- data/lib/grant/thread_status.rb +34 -0
- data/lib/grant/user.rb +16 -0
- data/lib/grant/version.rb +3 -0
- data/spec/config_parser_spec.rb +61 -0
- data/spec/integration_spec.rb +66 -0
- data/spec/model_security_spec.rb +162 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/thread_local_spec.rb +14 -0
- data/spec/thread_status_spec.rb +16 -0
- data/spec/user_spec.rb +25 -0
- metadata +106 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Near Infinity Corporation
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
= Grant
|
2
|
+
|
3
|
+
Grant is a Ruby gem and Rails plugin that forces you to make explicit security decisions about the operations performed on your ActiveRecord models. It provides a declarative way to specify rules granting permission to perform CRUD operations on ActiveRecord objects.
|
4
|
+
|
5
|
+
Grant does not allow you to specify which operations are restricted. Instead, it restricts all CRUD operations unless they're explicitly granted to the user. It also restricts adding or removing items to/from has_many and has_and_belongs_to_many associations. Only allowing operations explicitly granted forces you to make conscious security decisions. Grant will not help you make those decisions, but it won't let you forget to.
|
6
|
+
|
7
|
+
Additional information beyond that found in this README is available on the wiki[https://github.com/nearinfinity/grant/wiki].
|
8
|
+
|
9
|
+
= Installation
|
10
|
+
|
11
|
+
To install the Grant gem, simply run
|
12
|
+
|
13
|
+
gem install grant
|
14
|
+
|
15
|
+
To use it with a Rails 3 project or other project using Bundler, add the following line to your Gemfile
|
16
|
+
|
17
|
+
gem 'grant'
|
18
|
+
|
19
|
+
For your Rails 2.x project, add the following to your environment.rb file
|
20
|
+
|
21
|
+
config.gem 'grant'
|
22
|
+
|
23
|
+
Lastly, Grant can also be installed as a Rails plugin
|
24
|
+
|
25
|
+
script/plugin install git://github.com/nearinfinity/grant.git
|
26
|
+
|
27
|
+
= Setup
|
28
|
+
|
29
|
+
Grant needs to know who the current user is, but with no standard for doing so you'll have to do a little work to set things up. You simply need to set your current user model object as the Grant current user before any CRUD operations are performed. For example, in a Rails application you could add the following to your application_controller.rb
|
30
|
+
|
31
|
+
class ApplicationController < ActionController::Base
|
32
|
+
before_filter :set_current_user
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def set_current_user
|
37
|
+
Grant::User.current_user = @current_user
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
= Usage
|
42
|
+
|
43
|
+
To enable model security you simply include the Grant::ModelSecurity module in your model class. In the example below you see three 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. Similarly, the third grant statement permits additions and removals from the tags association when it's block evaluates to true. A Grant::Error is raised if any grant block evaluates to false or nil.
|
44
|
+
|
45
|
+
class Book < ActiveRecord::Base
|
46
|
+
include Grant::ModelSecurity
|
47
|
+
|
48
|
+
has_many :tags
|
49
|
+
grant(:find) { true }
|
50
|
+
grant(:create, :update, :destroy) { |user, model| model.editable_by_user? user }
|
51
|
+
grant(:add => :tags, :remove => :tags) { |user, model, associated_model| model.editable_by_user? user }
|
52
|
+
|
53
|
+
def editable_by_user? user
|
54
|
+
user.administrator? || user.has_role?(:editor)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
The valid actions to pass to a grant statement are :find, :create, :update, :destroy, :add, and :remove. The first four options are passed as symbols while :add and :remove are hash keys to association names they protect. Any number of options can be passed to a single grant statement, which is very useful if each of the actions share the same logic for determining access.
|
59
|
+
|
60
|
+
= Integration
|
61
|
+
|
62
|
+
There may be some instances where you need to perform an action on your model object without Grant stepping in and stopping you. In those cases you can include the Grant::Integration module for help.
|
63
|
+
|
64
|
+
class BooksController < ApplicationController
|
65
|
+
include Grant::Integration
|
66
|
+
|
67
|
+
def update
|
68
|
+
book = Book.find(params[:id])
|
69
|
+
without_grant { book.update_attributes(params[:book]) } # Grant is disabled for the entire block
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Copyright (c) 2010 Near Infinity Corporation, released under the MIT license
|
data/lib/grant.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Grant
|
2
|
+
class ConfigParser
|
3
|
+
|
4
|
+
def self.extract_config(args)
|
5
|
+
hash = (args.pop if args.last.is_a?(::Hash)) || {}
|
6
|
+
normalize_config args, hash
|
7
|
+
validate_config args, hash
|
8
|
+
|
9
|
+
[args, hash]
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def self.normalize_config(actions, associations)
|
15
|
+
actions.each_with_index { |item, index| actions[index] = item.to_sym unless item.kind_of? Symbol }
|
16
|
+
associations.each_pair { |k, v| associations[k.to_sym] = associations.delete(k) unless k.kind_of? Symbol }
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.validate_config(actions, associations)
|
20
|
+
raise Grant::Error.new("at least one :create, :find, :update, or :destroy action must be specified") if actions.empty? && associations.empty?
|
21
|
+
raise Grant::Error.new(":create, :find, :update, and :destroy are the only valid actions") unless actions.all? { |a| [:create, :find, :update, :destroy].include? a }
|
22
|
+
raise Grant::Error.new(":add and :remove are the only valid association specifications") unless associations.keys.all? { |k| [:add, :remove].include? k }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'grant/thread_status'
|
2
|
+
|
3
|
+
module Grant
|
4
|
+
module Integration
|
5
|
+
|
6
|
+
def without_grant
|
7
|
+
previously_disabled = grant_disabled?
|
8
|
+
disable_grant
|
9
|
+
|
10
|
+
begin
|
11
|
+
result = yield if block_given?
|
12
|
+
ensure
|
13
|
+
enable_grant unless previously_disabled
|
14
|
+
end
|
15
|
+
|
16
|
+
result
|
17
|
+
end
|
18
|
+
|
19
|
+
def with_grant
|
20
|
+
previously_disabled = grant_disabled?
|
21
|
+
enable_grant
|
22
|
+
|
23
|
+
begin
|
24
|
+
result = yield if block_given?
|
25
|
+
ensure
|
26
|
+
disable_grant if previously_disabled
|
27
|
+
end
|
28
|
+
|
29
|
+
result
|
30
|
+
end
|
31
|
+
|
32
|
+
def disable_grant
|
33
|
+
Grant::ThreadStatus.disable
|
34
|
+
end
|
35
|
+
|
36
|
+
def enable_grant
|
37
|
+
Grant::ThreadStatus.enable
|
38
|
+
end
|
39
|
+
|
40
|
+
def grant_disabled?
|
41
|
+
Grant::ThreadStatus.disabled?
|
42
|
+
end
|
43
|
+
|
44
|
+
def grant_enabled?
|
45
|
+
Grant::ThreadStatus.enabled?
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'grant/config_parser'
|
2
|
+
require 'grant/user'
|
3
|
+
require 'grant/thread_status'
|
4
|
+
|
5
|
+
module Grant
|
6
|
+
module ModelSecurity
|
7
|
+
|
8
|
+
def self.included(base)
|
9
|
+
[:create, :update, :destroy, :find].each do |action|
|
10
|
+
callback = (action == :find ? "after_#{action}" : "before_#{action}")
|
11
|
+
base.class_eval <<-RUBY
|
12
|
+
def grant_#{callback}
|
13
|
+
grant_raise_error(grant_current_user, '#{action}', self) unless grant_disabled?
|
14
|
+
end
|
15
|
+
RUBY
|
16
|
+
base.send callback.to_sym, "grant_#{callback}".to_sym
|
17
|
+
end
|
18
|
+
|
19
|
+
base.extend ClassMethods
|
20
|
+
end
|
21
|
+
|
22
|
+
# ActiveRecord won't call the after_find handler unless it see's a specific after_find method defined
|
23
|
+
def after_find; end
|
24
|
+
|
25
|
+
def grant_current_user
|
26
|
+
Grant::User.current_user
|
27
|
+
end
|
28
|
+
|
29
|
+
def grant_disabled?
|
30
|
+
Grant::ThreadStatus.disabled? || @grant_disabled
|
31
|
+
end
|
32
|
+
|
33
|
+
def grant_raise_error(user, action, model, association_id=nil, associated_model=nil)
|
34
|
+
msg = ["#{action} permission",
|
35
|
+
"not granted to #{user.class.name}:#{user.id}",
|
36
|
+
"for resource #{model.class.name}:#{model.id}"]
|
37
|
+
msg.insert(1, "to #{association_id}:#{associated_model.class.name} association") if association_id && associated_model
|
38
|
+
|
39
|
+
raise Grant::Error.new(msg.join(' '))
|
40
|
+
end
|
41
|
+
|
42
|
+
module ClassMethods
|
43
|
+
def grant(*args, &blk)
|
44
|
+
actions, associations = Grant::ConfigParser.extract_config(args)
|
45
|
+
|
46
|
+
associations.each_pair do |action, association_ids|
|
47
|
+
Array(association_ids).each do |association_id|
|
48
|
+
grant_callback = "grant_#{action}_#{association_id}".to_sym
|
49
|
+
define_method(grant_callback) do |associated_model|
|
50
|
+
grant_raise_error(grant_current_user, action, self, association_id, associated_model) unless grant_disabled? || blk.call(grant_current_user, self, associated_model)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
actions.each do |action|
|
56
|
+
grant_callback = (action.to_sym == :find ? "grant_after_find" : "grant_before_#{action}").to_sym
|
57
|
+
define_method(grant_callback) do
|
58
|
+
grant_raise_error(grant_current_user, action, self) unless grant_disabled? || blk.call(grant_current_user, self)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def has_and_belongs_to_many(association_id, options={}, &extension)
|
64
|
+
add_grant_association_callback(:add, association_id, options)
|
65
|
+
add_grant_association_callback(:remove, association_id, options)
|
66
|
+
super
|
67
|
+
end
|
68
|
+
|
69
|
+
def has_many(association_id, options={}, &extension)
|
70
|
+
add_grant_association_callback(:add, association_id, options)
|
71
|
+
add_grant_association_callback(:remove, association_id, options)
|
72
|
+
super
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
def add_grant_association_callback(action, association_id, options)
|
78
|
+
callback_name = "before_#{action}".to_sym
|
79
|
+
callback = "grant_#{action}_#{association_id}".to_sym
|
80
|
+
unless self.instance_methods.include? callback.to_s
|
81
|
+
class_eval <<-RUBY
|
82
|
+
def #{callback}(associated_model)
|
83
|
+
grant_raise_error(grant_current_user, '#{action}', self, '#{association_id}', associated_model) unless grant_disabled?
|
84
|
+
end
|
85
|
+
RUBY
|
86
|
+
end
|
87
|
+
|
88
|
+
if options.has_key? callback_name
|
89
|
+
if options[callback_name].kind_of? Array
|
90
|
+
options[callback_name].insert(0, callback)
|
91
|
+
else
|
92
|
+
options[callback_name] = [callback, options[callback_name]]
|
93
|
+
end
|
94
|
+
else
|
95
|
+
options[callback_name] = callback
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'grant/integration'
|
2
|
+
|
3
|
+
module Grant
|
4
|
+
module SpecHelpers
|
5
|
+
include Grant::Integration
|
6
|
+
|
7
|
+
def self.included(base)
|
8
|
+
base.class_eval do
|
9
|
+
before(:each) do
|
10
|
+
disable_grant
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
enable_grant
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Grant
|
2
|
+
class ThreadLocal
|
3
|
+
|
4
|
+
def initialize(initial_value)
|
5
|
+
@thread_symbol = "#{rand}#{Time.now.to_f}"
|
6
|
+
set initial_value
|
7
|
+
end
|
8
|
+
|
9
|
+
def set(value)
|
10
|
+
Thread.current[@thread_symbol] = value
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
Thread.current[@thread_symbol]
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'grant/thread_local'
|
2
|
+
|
3
|
+
module Grant
|
4
|
+
module ThreadStatus
|
5
|
+
|
6
|
+
def self.enabled?
|
7
|
+
status
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.disabled?
|
11
|
+
!status
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.enable
|
15
|
+
set_status true
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.disable
|
19
|
+
set_status false
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def self.status
|
24
|
+
@status = Grant::ThreadLocal.new(true) if @status.nil?
|
25
|
+
@status.get
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.set_status(status)
|
29
|
+
@status = Grant::ThreadLocal.new(true) if @status.nil?
|
30
|
+
@status.set status
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
data/lib/grant/user.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Grant
|
2
|
+
module User
|
3
|
+
def current_user
|
4
|
+
Thread.current[@@current_user_symbol]
|
5
|
+
end
|
6
|
+
|
7
|
+
def current_user=(user)
|
8
|
+
Thread.current[@@current_user_symbol] = user
|
9
|
+
end
|
10
|
+
|
11
|
+
module_function :current_user, :current_user=
|
12
|
+
|
13
|
+
private
|
14
|
+
@@current_user_symbol = :grant_current_user_symbol
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant'
|
3
|
+
|
4
|
+
describe Grant::ConfigParser do
|
5
|
+
|
6
|
+
describe 'Configuration' do
|
7
|
+
it "should parse actions and associations from a config array" do
|
8
|
+
config = Grant::ConfigParser.extract_config([:create, 'update', {:add => [:people, :places], 'remove' => :people}])
|
9
|
+
config.should_not be_nil
|
10
|
+
config.should have(2).items
|
11
|
+
config[0].should =~ [:create, :update]
|
12
|
+
config[1].should == {:add => [:people, :places], :remove => :people}
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should parse actions from a config array when associations are absent" do
|
16
|
+
config = Grant::ConfigParser.extract_config([:create, :update])
|
17
|
+
config.should_not be_nil
|
18
|
+
config.should have(2).items
|
19
|
+
config[0].should =~ [:create, :update]
|
20
|
+
config[1].should == {}
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should parse actions and associations from a config array when options are absent" do
|
24
|
+
config = Grant::ConfigParser.extract_config([:create, 'update', {:add => ['people', :places]}])
|
25
|
+
config.should_not be_nil
|
26
|
+
config.should have(2).items
|
27
|
+
config[0].should =~ [:create, :update]
|
28
|
+
config[1].should == {:add => ['people', :places]}
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should parse actions" do
|
32
|
+
config = Grant::ConfigParser.extract_config([:create])
|
33
|
+
config.should_not be_nil
|
34
|
+
config.should have(2).items
|
35
|
+
config[0].should =~ [:create]
|
36
|
+
config[1].should == {}
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'Configuration Validation' do
|
42
|
+
it "should raise a Grant::Error if no action or association is specified" do
|
43
|
+
lambda {
|
44
|
+
Grant::ConfigParser.instance_eval { validate_config([], {}) }
|
45
|
+
}.should raise_error(Grant::Error)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should raise a Grant::Error if an invalid action is specified" do
|
49
|
+
lambda {
|
50
|
+
Grant::ConfigParser.instance_eval { validate_config([:create, :udate], {:add => [:people, :places]}) }
|
51
|
+
}.should raise_error(Grant::Error)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should raise a Grant::Error if an invalid association is specified" do
|
55
|
+
lambda {
|
56
|
+
Grant::ConfigParser.instance_eval { validate_config([:destroy], {:add => :people, :update => :places}) }
|
57
|
+
}.should raise_error(Grant::Error)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant'
|
3
|
+
|
4
|
+
describe Grant::Integration do
|
5
|
+
include Grant::Integration
|
6
|
+
|
7
|
+
it "should have the ability to disable Grant" do
|
8
|
+
disable_grant
|
9
|
+
grant_disabled?.should be_true
|
10
|
+
Grant::ThreadStatus.should be_disabled
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should have the ability to enable Grant" do
|
14
|
+
disable_grant
|
15
|
+
enable_grant
|
16
|
+
grant_enabled?.should be_true
|
17
|
+
Grant::ThreadStatus.should be_enabled
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should have the ability to check if grant is enabled" do
|
21
|
+
enable_grant
|
22
|
+
grant_enabled?.should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should have the ability to check if grant is disabled" do
|
26
|
+
disable_grant
|
27
|
+
grant_disabled?.should be_true
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be able to execute a block of code with grant temporarily disabled but switched back to enabled afterwards" do
|
31
|
+
enable_grant
|
32
|
+
without_grant do
|
33
|
+
grant_disabled?.should be_true
|
34
|
+
Grant::ThreadStatus.should be_disabled
|
35
|
+
end
|
36
|
+
grant_enabled?.should be_true
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should be able to execute a block of code with grant disabled and remain disabled afterwards if it was beforehand" do
|
40
|
+
disable_grant
|
41
|
+
without_grant do
|
42
|
+
grant_disabled?.should be_true
|
43
|
+
Grant::ThreadStatus.should be_disabled
|
44
|
+
end
|
45
|
+
grant_disabled?.should be_true
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should be able to execute a block of code with grant temporarily enabled but switched back to disabled afterwards" do
|
49
|
+
disable_grant
|
50
|
+
with_grant do
|
51
|
+
grant_enabled?.should be_true
|
52
|
+
Grant::ThreadStatus.should be_enabled
|
53
|
+
end
|
54
|
+
grant_disabled?.should be_true
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should be able to execute a block of code with grant enabled and remain enabled afterwards if it was beforehand" do
|
58
|
+
enable_grant
|
59
|
+
with_grant do
|
60
|
+
grant_enabled?.should be_true
|
61
|
+
Grant::ThreadStatus.should be_enabled
|
62
|
+
end
|
63
|
+
grant_enabled?.should be_true
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant'
|
3
|
+
|
4
|
+
describe Grant::ModelSecurity do
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
Grant::User.current_user = (Class.new do
|
8
|
+
def id; 1 end
|
9
|
+
end).new
|
10
|
+
end
|
11
|
+
|
12
|
+
describe 'module include' do
|
13
|
+
it 'should establish failing ActiveRecord callbacks for before_create, before_update, before_destroy, and after_find when included' do
|
14
|
+
verify_standard_callbacks(new_model_class.new)
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should establish failing ActiveRecord callbacks for any has_many or has_and_belongs_to_many associations' do
|
18
|
+
c = new_model_class
|
19
|
+
c.instance_eval do
|
20
|
+
has_many :users
|
21
|
+
has_and_belongs_to_many :groups
|
22
|
+
end
|
23
|
+
verify_association_callbacks(c.new)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '#grant' do
|
28
|
+
it 'should allow after_find callback to succeed when granted' do
|
29
|
+
c = new_model_class.instance_eval { grant(:find) { true }; self }
|
30
|
+
verify_standard_callbacks(c.new, :find)
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'should allow before_create callback to succeed when granted' do
|
34
|
+
c = new_model_class.instance_eval { grant(:create) { true }; self }
|
35
|
+
verify_standard_callbacks(c.new, :create)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'should allow before_update callback to succeed when granted' do
|
39
|
+
c = new_model_class.instance_eval { grant(:update) { true }; self }
|
40
|
+
verify_standard_callbacks(c.new, :update)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'should allow before_destroy callback to succeed when granted' do
|
44
|
+
c = new_model_class.instance_eval { grant(:destroy) { true }; self }
|
45
|
+
verify_standard_callbacks(c.new, :destroy)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'should allow multiple callbacks to be specified with one grant statment' do
|
49
|
+
c = new_model_class.instance_eval { grant(:create, :update) { true }; self }
|
50
|
+
verify_standard_callbacks(c.new, :create, :update)
|
51
|
+
|
52
|
+
c = new_model_class.instance_eval { grant(:create, :update, :destroy) { true }; self }
|
53
|
+
verify_standard_callbacks(c.new, :create, :update, :destroy)
|
54
|
+
|
55
|
+
c = new_model_class.instance_eval { grant(:create, :update, :destroy, :find) { true }; self }
|
56
|
+
verify_standard_callbacks(c.new, :create, :update, :destroy, :find)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'should allow adding to an association to succeed when granted' do
|
60
|
+
c = new_model_class
|
61
|
+
c.instance_eval do
|
62
|
+
has_many :users
|
63
|
+
has_and_belongs_to_many :groups
|
64
|
+
grant(:add => [:users, :groups]) { true }
|
65
|
+
end
|
66
|
+
verify_association_callbacks(c.new, :add_users, :add_groups)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'should allow adding to an association to succeed when granted above association declarations' do
|
70
|
+
c = new_model_class
|
71
|
+
c.instance_eval do
|
72
|
+
grant(:add => [:users, :groups]) { true }
|
73
|
+
has_many :users
|
74
|
+
has_and_belongs_to_many :groups, :before_add => [:bogus1, :bogus2]
|
75
|
+
define_method(:bogus1) {}
|
76
|
+
define_method(:bogus2) {}
|
77
|
+
end
|
78
|
+
verify_association_callbacks(c.new, :add_users, :add_groups)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should allow removing from an association to succeed when granted' do
|
82
|
+
c = new_model_class
|
83
|
+
c.instance_eval do
|
84
|
+
has_many :users
|
85
|
+
has_and_belongs_to_many :groups, :before_remove => :bogus1
|
86
|
+
grant(:remove => [:users, :groups]) { true }
|
87
|
+
define_method(:bogus1) {}
|
88
|
+
end
|
89
|
+
verify_association_callbacks(c.new, :remove_users, :remove_groups)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'should allow removing from an association to succeed when granted above association declarations' do
|
93
|
+
c = new_model_class
|
94
|
+
c.instance_eval do
|
95
|
+
grant(:remove => [:users, :groups]) { true }
|
96
|
+
has_many :users
|
97
|
+
has_and_belongs_to_many :groups
|
98
|
+
end
|
99
|
+
verify_association_callbacks(c.new, :remove_users, :remove_groups)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def verify_standard_callbacks(instance, *succeeding_callbacks)
|
104
|
+
verify_callbacks([:create, :update, :destroy, :find], instance, nil, succeeding_callbacks)
|
105
|
+
end
|
106
|
+
|
107
|
+
def verify_association_callbacks(instance, *succeeding_callbacks)
|
108
|
+
verify_callbacks([:add_users, :remove_users, :add_groups, :remove_groups], instance, new_model_class.new, succeeding_callbacks)
|
109
|
+
end
|
110
|
+
|
111
|
+
def verify_callbacks(all_actions, instance, associated_model, succeeding_callbacks)
|
112
|
+
all_actions.each do |action|
|
113
|
+
expectation = succeeding_callbacks.include?(action) ? :should_not : :should
|
114
|
+
lambda { instance.send(action, associated_model) }.send(expectation, raise_error(Grant::Error))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def new_model_class
|
119
|
+
Class.new(ActiveRecordMock) do
|
120
|
+
include Grant::ModelSecurity
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class ActiveRecordMock
|
125
|
+
def id; 1 end
|
126
|
+
|
127
|
+
def self.before_create(method)
|
128
|
+
define_method(:create) { send method }
|
129
|
+
end
|
130
|
+
|
131
|
+
def self.before_update(method)
|
132
|
+
define_method(:update) { send method }
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.before_destroy(method)
|
136
|
+
define_method(:destroy) { send method }
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.after_find(method)
|
140
|
+
define_method(:find) { send method }
|
141
|
+
end
|
142
|
+
|
143
|
+
def self.has_many(association_id, options, &extension)
|
144
|
+
define_method("add_#{association_id}".to_sym) do |associated_model|
|
145
|
+
Array(options[:before_add]).each { |callback| send(callback, associated_model) }
|
146
|
+
end
|
147
|
+
define_method("remove_#{association_id}".to_sym) do |associated_model|
|
148
|
+
Array(options[:before_remove]).each { |callback| send(callback, associated_model) }
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def self.has_and_belongs_to_many(association_id, options, &extension)
|
153
|
+
define_method("add_#{association_id}".to_sym) do |associated_model|
|
154
|
+
Array(options[:before_add]).each { |callback| send(callback, associated_model) }
|
155
|
+
end
|
156
|
+
define_method("remove_#{association_id}".to_sym) do |associated_model|
|
157
|
+
Array(options[:before_remove]).each { |callback| send(callback, associated_model) }
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'rspec'
|
2
|
+
|
3
|
+
# Requires supporting files with custom matchers and macros, etc,
|
4
|
+
# in ./support/ and its subdirectories.
|
5
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
6
|
+
|
7
|
+
RSpec.configure do |config|
|
8
|
+
# If you're not using ActiveRecord you should remove these
|
9
|
+
# lines, delete config/database.yml and disable :active_record
|
10
|
+
# in your config/boot.rb
|
11
|
+
# config.use_transactional_fixtures = true
|
12
|
+
# config.use_instantiated_fixtures = false
|
13
|
+
|
14
|
+
# == Fixtures
|
15
|
+
#
|
16
|
+
# You can declare fixtures for each example_group like this:
|
17
|
+
# describe "...." do
|
18
|
+
# fixtures :table_a, :table_b
|
19
|
+
#
|
20
|
+
# Alternatively, if you prefer to declare them only once, you can
|
21
|
+
# do so right here. Just uncomment the next line and replace the fixture
|
22
|
+
# names with your fixtures.
|
23
|
+
#
|
24
|
+
# config.global_fixtures = :all
|
25
|
+
#
|
26
|
+
# If you declare global fixtures, be aware that they will be declared
|
27
|
+
# for all of your examples, even those that don't use them.
|
28
|
+
#
|
29
|
+
# You can also declare which fixtures to use (for example fixtures for test/fixtures):
|
30
|
+
#
|
31
|
+
# config.fixture_path = RAILS_ROOT + '/spec/fixtures/'
|
32
|
+
#
|
33
|
+
# == Mock Framework
|
34
|
+
#
|
35
|
+
# RSpec uses it's own mocking framework by default. If you prefer to
|
36
|
+
# use mocha, flexmock or RR, uncomment the appropriate line:
|
37
|
+
#
|
38
|
+
# config.mock_with :mocha
|
39
|
+
# config.mock_with :flexmock
|
40
|
+
# config.mock_with :rr
|
41
|
+
#
|
42
|
+
# == Notes
|
43
|
+
#
|
44
|
+
# For more information take a look at Spec::Runner::Configuration and Spec::Runner
|
45
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant/thread_local'
|
3
|
+
|
4
|
+
describe Grant::ThreadLocal do
|
5
|
+
it "should properly set and get thread-local variables" do
|
6
|
+
val = "val"
|
7
|
+
tl = Grant::ThreadLocal.new(val)
|
8
|
+
tl.get.should == val
|
9
|
+
|
10
|
+
val2 = "val2"
|
11
|
+
tl.set val2
|
12
|
+
tl.get.should == val2
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant'
|
3
|
+
|
4
|
+
describe Grant::ThreadStatus do
|
5
|
+
it "should be enabled if set to enabled" do
|
6
|
+
Grant::ThreadStatus.enable
|
7
|
+
Grant::ThreadStatus.should be_enabled
|
8
|
+
Grant::ThreadStatus.should_not be_disabled
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should be disabled if set to disabled" do
|
12
|
+
Grant::ThreadStatus.disable
|
13
|
+
Grant::ThreadStatus.should_not be_enabled
|
14
|
+
Grant::ThreadStatus.should be_disabled
|
15
|
+
end
|
16
|
+
end
|
data/spec/user_spec.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
require 'grant/user'
|
3
|
+
|
4
|
+
describe Grant::User do
|
5
|
+
it "should return the same user that's set on the same thread" do
|
6
|
+
user = "user"
|
7
|
+
Grant::User.current_user = user
|
8
|
+
Grant::User.current_user.should == user
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should not return the same user from a different thread" do
|
12
|
+
user = "user"
|
13
|
+
user2 = "user2"
|
14
|
+
|
15
|
+
Grant::User.current_user = user
|
16
|
+
|
17
|
+
Thread.new do
|
18
|
+
Grant::User.current_user.should be_nil
|
19
|
+
Grant::User.current_user = user2
|
20
|
+
Grant::User.current_user.should == user2
|
21
|
+
end
|
22
|
+
|
23
|
+
Grant::User.current_user.should == user
|
24
|
+
end
|
25
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grant
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 21
|
5
|
+
prerelease: false
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 1.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Jeff Kunkle
|
14
|
+
- Matt Wizeman
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2010-12-29 00:00:00 -05:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: rspec
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 3
|
31
|
+
segments:
|
32
|
+
- 0
|
33
|
+
version: "0"
|
34
|
+
type: :development
|
35
|
+
version_requirements: *id001
|
36
|
+
description: Grant is a Ruby gem and Rails plugin that forces you to make explicit security decisions about the operations performed on your ActiveRecord models.
|
37
|
+
email:
|
38
|
+
executables: []
|
39
|
+
|
40
|
+
extensions: []
|
41
|
+
|
42
|
+
extra_rdoc_files: []
|
43
|
+
|
44
|
+
files:
|
45
|
+
- lib/grant/config_parser.rb
|
46
|
+
- lib/grant/integration.rb
|
47
|
+
- lib/grant/model_security.rb
|
48
|
+
- lib/grant/spec_helpers.rb
|
49
|
+
- lib/grant/thread_local.rb
|
50
|
+
- lib/grant/thread_status.rb
|
51
|
+
- lib/grant/user.rb
|
52
|
+
- lib/grant/version.rb
|
53
|
+
- lib/grant.rb
|
54
|
+
- LICENSE
|
55
|
+
- README.rdoc
|
56
|
+
- spec/config_parser_spec.rb
|
57
|
+
- spec/integration_spec.rb
|
58
|
+
- spec/model_security_spec.rb
|
59
|
+
- spec/spec_helper.rb
|
60
|
+
- spec/thread_local_spec.rb
|
61
|
+
- spec/thread_status_spec.rb
|
62
|
+
- spec/user_spec.rb
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: http://github.com/nearinfinity/grant
|
65
|
+
licenses:
|
66
|
+
- MIT
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 23
|
87
|
+
segments:
|
88
|
+
- 1
|
89
|
+
- 3
|
90
|
+
- 6
|
91
|
+
version: 1.3.6
|
92
|
+
requirements: []
|
93
|
+
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.3.7
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: Conscious security constraints for your ActiveRecord model objects
|
99
|
+
test_files:
|
100
|
+
- spec/config_parser_spec.rb
|
101
|
+
- spec/integration_spec.rb
|
102
|
+
- spec/model_security_spec.rb
|
103
|
+
- spec/spec_helper.rb
|
104
|
+
- spec/thread_local_spec.rb
|
105
|
+
- spec/thread_status_spec.rb
|
106
|
+
- spec/user_spec.rb
|