permissive 0.0.1 → 0.2.0.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/.gemspec CHANGED
@@ -19,7 +19,25 @@ Gem::Specification.new do |s|
19
19
  "README.markdown"
20
20
  ]
21
21
  s.files = [
22
- "VERSION"
22
+ ".gemspec",
23
+ ".gitignore",
24
+ "MIT-LICENSE",
25
+ "README.markdown",
26
+ "Rakefile",
27
+ "VERSION",
28
+ "generators/permissive_migration/USAGE",
29
+ "generators/permissive_migration/permissive_migration_generator.rb",
30
+ "generators/permissive_migration/templates/permissive_migration.rb",
31
+ "lib/permissive.rb",
32
+ "lib/permissive/acts_as_permissive.rb",
33
+ "lib/permissive/permission.rb",
34
+ "lib/permissive/permissions.rb",
35
+ "rails/init.rb",
36
+ "spec/acts_as_permissive_spec.rb",
37
+ "spec/permissions_spec.rb",
38
+ "spec/rcov.opts",
39
+ "spec/spec.opts",
40
+ "spec/spec_helper.rb"
23
41
  ]
24
42
  s.homepage = %q{http://github.com/flipsasser/permissive}
25
43
  s.rdoc_options = ["--charset=UTF-8"]
data/.gitignore CHANGED
@@ -1,2 +1,4 @@
1
1
  *.gem
2
- pkg/
2
+ coverage/
3
+ pkg/
4
+ **/*.log
data/CHANGELOG ADDED
File without changes
data/README.markdown CHANGED
@@ -1,8 +1,9 @@
1
1
  Permissive gives your ActiveRecord models granular permission support
2
2
  =
3
- Permissive combines a model-based permissions system with bitmasking to
4
- create a flexible approach to maintaining permissions on your ActiveRecord
5
- models. It supports an easy-to-use set of methods for accessing and
3
+ Permissive makes it trivial to add complex permission granting and checking
4
+ to your applications using ActiveRecord. It combines a model-based permissions
5
+ system with bitmasking to create a flexible approach to maintaining permissions
6
+ on your models. It supports an easy-to-use set of methods for accessing and
6
7
  determining permissions, including some fun metaprogramming.
7
8
 
8
9
  Installation
@@ -25,21 +26,30 @@ Installation
25
26
  Usage
26
27
  -
27
28
 
28
- First, define a few permissions constants. We'll define them in `Rails.root/config/initializers/permissive.rb`. The best practice is to name them in a verb format that follows this pattern: "Object can `DO_PERMISSION_NAME`".
29
+ First, define a few permissions on an ActiveRecord::Base subclass. You define them using the following simple, block-based API:
29
30
 
30
- Permission constants need to be int values counting up from zero. We use ints because Permissive uses bit masking to keep permissions data compact and performant.
31
-
32
- module Permissive::Permissions
33
- MANAGE_GAMES = 0
34
- CONTROL_RIDES = 1
35
- PUNCH = 2
31
+ class User < ActiveRecord::Base
32
+ has_permissions do
33
+ to :manage_games, 0
34
+ to :control_rides, 1
35
+ to :punch, 2
36
+ end
36
37
  end
37
38
 
39
+ The best practice is to name them in a verb format that follows this pattern: "Object can `do_action_name`".
40
+
41
+ Permission values (the second argument in `to`) need to be int values counting up from zero. We use ints because Permissive uses bit
42
+ masking to keep permissions data compact and performant.
43
+
38
44
  And that's all it takes to configure permissions! Now that we have them, let's grant them to a model or two:
39
45
 
40
46
  class Employee < ActiveRecord::Base
41
- acts_as_permissive
42
- validates_presence_of :first_name, :last_name
47
+ has_permissions, :on => :companies do
48
+ to :manage_games, 0
49
+ to :control_rides, 1
50
+ to :punch, 2
51
+ end
52
+ validates_presence_of :first_name, :last_name
43
53
  end
44
54
 
45
55
  class Company < ActiveRecord::Base
@@ -59,8 +69,9 @@ Easy-peasy, right? Let's try granting a few permissions:
59
69
  @james.can?(:manage_games, :on => @adventureland) #=> true
60
70
 
61
71
  # We can also use the metaprogramming syntax:
62
- @james.can_manage_games_on?(@adventureland) #=> true
63
- @james.can_control_rides_on?(@adventureland) #=> false
72
+ @james.can_manage_games_in! @adventureland
73
+ @james.can_manage_games_in? @adventureland #=> true
74
+ @james.can_control_rides_in? @adventureland #=> false
64
75
 
65
76
  # We can check for multiple permissions, too:
66
77
  @james.can?(:manage_games, :control_rides) #=> false
@@ -69,17 +80,20 @@ Easy-peasy, right? Let's try granting a few permissions:
69
80
 
70
81
  # Scoping can be done through any object
71
82
  @frigo.can!(:punch, :on => @james)
72
- @frigo.can_punch_on?(@james) #=> true
83
+ @frigo.can_punch? @james #=> true
73
84
 
74
85
  # And the permissions aren't reciprocal
75
- @james.can_punch_on?(@frigo) #=> false
86
+ @james.can_punch? @frigo #=> false
76
87
 
77
88
  # Of course, we can grant global (non-scoped) permissions, too:
78
- @frigo.can!(:control_rides)
89
+ @frigo.can_control_rides!
79
90
  @frigo.can_control_rides? #=> true
80
91
 
92
+ # And we can grant permissions global to a class:
93
+ @frigo.can_control_rides_in! Company
94
+
81
95
  # BUT! Global permissions don't override scoped permissions.
82
- @frigo.can_control_rides_on?(@adventureland) #=> false
96
+ @frigo.can_control_rides_in?(@adventureland) #=> false
83
97
 
84
98
  # Likewise, scoped permissions don't bubble up globally:
85
99
  @james.can_manage_games? #=> false
@@ -90,6 +104,10 @@ Easy-peasy, right? Let's try granting a few permissions:
90
104
  # We can revoke all permissions, in any scope, too:
91
105
  @frigo.revoke(:all)
92
106
 
107
+ # And revoking does the fun meta thing, too:
108
+ @frigo.cannot_punch!(@james)
109
+ @frigo.can_punch? @james #=> flase
110
+
93
111
  And that's it!
94
112
 
95
113
  Scoping
@@ -128,17 +146,7 @@ which might yield something like
128
146
  # and
129
147
  @employee.can_control_rides_in_company @adventureland
130
148
 
131
- I'd also like to support a more intelligent grammar:
132
-
133
- @james.can_punch? @frigo
134
- @frigo.can!(:control_rides, :in => @adventureland)
135
-
136
- Meta-programmed methods for granting and revoking would be cool, too:
137
-
138
- @james.can_punch! @frigo
139
- @frigo.cannot_control_rides_in! @adventureland
140
-
141
- And while we're on the subject of metaprogramming, let's add some OR-ing to the whole thing:
149
+ Let's add some OR-ing to the whole thing:
142
150
 
143
151
  @james.can_control_rides_or_manage_games_in? @adventureland
144
152
 
data/Rakefile CHANGED
@@ -15,7 +15,7 @@ namespace :spec do
15
15
  Spec::Rake::SpecTask.new(:rcov) do |t|
16
16
  t.spec_files = FileList['spec/**/*.rb']
17
17
  t.rcov = true
18
- t.rcov_opts = ['--exclude', 'examples']
18
+ t.rcov_opts = ['--exclude', 'spec/*,gems/*']
19
19
  end
20
20
  end
21
21
 
@@ -30,7 +30,7 @@ begin
30
30
  determining permissions, including some fun metaprogramming.}
31
31
  gemspec.email = "flip@x451.com"
32
32
  gemspec.homepage = "http://github.com/flipsasser/permissive"
33
- gemspec.authors = ["Flip Sasser", "Simon Parsons"]
33
+ gemspec.authors = ["Flip Sasser"]
34
34
  end
35
35
  rescue LoadError
36
36
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.1
1
+ 0.2.0.alpha
@@ -6,12 +6,10 @@ class InstallPermissive < ActiveRecord::Migration
6
6
  t.integer :scoped_object_id
7
7
  t.string :scoped_object_type, :limit => 32
8
8
  t.integer :mask, :default => 0
9
- t.integer :grant_mask, :default => 0
10
9
  end
11
10
  add_index :permissive_permissions, [:permitted_object_id, :permitted_object_type], :name => 'permissive_permitted'
12
11
  add_index :permissive_permissions, [:scoped_object_id, :scoped_object_type], :name => 'permissive_scoped'
13
12
  add_index :permissive_permissions, :mask, :name => 'permissive_masks'
14
- add_index :permissive_permissions, :grant_mask, :name => 'permissive_grant_masks'
15
13
  end
16
14
 
17
15
  def self.down
data/lib/permissive.rb CHANGED
@@ -1,15 +1,17 @@
1
- require 'permissive/permission'
2
- require 'permissive/acts_as_permissive'
3
- require 'permissive/permissions'
1
+ require 'permissive/errors'
2
+ require 'permissive/has_permissions'
4
3
 
5
4
  module Permissive
6
- @@strong = false
7
-
8
- def self.strong
9
- @@strong
10
- end
11
-
12
- def self.strong=(new_strong)
13
- @@strong = !!new_strong
14
- end
5
+ # @@strong = false
6
+ #
7
+ # def self.strong
8
+ # @@strong
9
+ # end
10
+ #
11
+ # def self.strong=(new_strong)
12
+ # @@strong = !!new_strong
13
+ # end
14
+ #
15
+ autoload(:Permission, 'permissive/permission')
16
+ autoload(:PermissionDefinition, 'permissive/permission_definition')
15
17
  end
@@ -0,0 +1,4 @@
1
+ module Permissive
2
+ class PermissionError < StandardError; end;
3
+ class InvalidPermissionError < StandardError; end;
4
+ end
@@ -0,0 +1,153 @@
1
+ module Permissive
2
+ module HasPermissions
3
+ module ClassMethods
4
+ def self.extended(base)
5
+
6
+ end
7
+
8
+ def has_permissions(options = {}, &block)
9
+ options.assert_valid_keys(:on)
10
+
11
+ # Define a permissions method. This will track scoped or global
12
+ # permission levels, depending on how you define them.
13
+ class_eval do
14
+ def self.permissions
15
+ @permissions ||= {}
16
+ end
17
+ end unless respond_to?(:permissions)
18
+
19
+ include InstanceMethods
20
+
21
+ has_many :permissions, :class_name => 'Permissive::Permission', :as => :permitted_object do
22
+ def can!(*args)
23
+ options = args.extract_options!
24
+ options.assert_valid_keys(:on, :reset)
25
+ permission_matcher = case options[:on]
26
+ when ActiveRecord::Base
27
+ permission = proxy_owner.permissions.find_or_initialize_by_scoped_object_id_and_scoped_object_type(options[:on].id, options[:on].class.name)
28
+ when Class
29
+ permission = proxy_owner.permissions.find_or_initialize_by_scoped_object_id_and_scoped_object_type(nil, options[:on].name)
30
+ when String, Symbol
31
+ class_name = Permissive::PermissionDefinition.interpolate_scope(proxy_owner.class, options[:on])
32
+ permission = proxy_owner.permissions.find_or_initialize_by_scoped_object_id_and_scoped_object_type(nil, class_name)
33
+ else
34
+ permission = Permissive::Permission.find_or_initialize_by_permitted_object_id_and_permitted_object_type_and_scoped_object_id_and_scoped_object_type(proxy_owner.id, proxy_owner.class.to_s, nil, nil)
35
+ end
36
+ if options[:reset]
37
+ permission.mask = 0
38
+ end
39
+ bits_for(options[:on], args).each do |bit|
40
+ unless permission.mask & bit != 0
41
+ permission.mask = permission.mask | bit
42
+ end
43
+ end
44
+ permission.save!
45
+ permission
46
+ end
47
+
48
+ def can?(*args)
49
+ options = args.extract_options!
50
+ options.assert_valid_keys(:in, :on)
51
+ options[:on] ||= options.delete(:in)
52
+ !on(options[:on]).granted(bits_for(options[:on], args)).empty?
53
+ end
54
+
55
+ def revoke(*args)
56
+ options = args.extract_options!
57
+ if args.length == 1 && args.first == :all
58
+ on(options[:on]).destroy_all
59
+ else
60
+ bits = bits_for(options[:on], args)
61
+ on(options[:on]).each do |permission|
62
+ bits.each do |bit|
63
+ if permission.mask & bit
64
+ permission.mask = permission.mask ^ bit
65
+ end
66
+ end
67
+ permission.save!
68
+ end
69
+ end
70
+ end
71
+
72
+ def bits_for(scope, permissions)
73
+ on = PermissionDefinition.normalize_scope(proxy_owner.class, scope)
74
+ permissions.map do |permission|
75
+ proxy_owner.class.permissions[on].try(:permissions).try(:[], permission.to_s.underscore.gsub('/', '_').to_sym) || raise(Permissive::InvalidPermissionError.new("#{proxy_owner.class.name} does not have a#{'n' if permission.to_s[0, 1].downcase =~ /[aeiou]/} #{permission} permission#{" on #{on}" if on}"))
76
+ end
77
+ end
78
+ private :bits_for
79
+ end
80
+
81
+ delegate :can!, :can?, :revoke, :to => :permissions
82
+
83
+ permission_definition = Permissive::PermissionDefinition.define(self, options, &block)
84
+
85
+ permission_setter = options[:on].nil? || options[:on] == :global ? 'permissions=' : "#{options[:on].to_s.singularize}_permissions="
86
+ class_eval <<-eoc
87
+ def #{permission_setter}(values)
88
+ if values.all? {|value| value.is_a?(String) || value.is_a?(Symbol)}
89
+ can!(values, :reset => true, :on => #{options[:on].inspect})
90
+ else
91
+ super
92
+ end
93
+ end
94
+ eoc
95
+
96
+
97
+ # Oh that's right, it'll return an object.
98
+ permission_definition
99
+ end
100
+ alias :has_permission :has_permissions
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+ def method_missing(method, *args)
106
+ if method.to_s =~ /^can(not){0,1}_([^\?]+)(\?|!)$/
107
+ revoke = $1 == "not"
108
+ permissions = $2
109
+ setter = $3 == "!"
110
+ options = args.extract_options!
111
+ options[:on] ||= args.pop
112
+ if permissions =~ /_(on|in)$/
113
+ permissions.chomp!("_#{$1}")
114
+ end
115
+ if options[:on]
116
+ scope = Permissive::PermissionDefinition.normalize_scope(self.class, options[:on])
117
+ else
118
+ scope = :global
119
+ end
120
+ permissions = permissions.split('_and_')
121
+ if permissions.all? {|permission| self.class.permissions[scope].permissions.has_key?(permission.downcase.to_sym) }
122
+ if revoke
123
+ class_eval <<-end_eval
124
+ def #{method}(scope = nil)
125
+ revoke(#{[permissions, args].flatten.join(', ').inspect}, :on => scope)
126
+ end
127
+ end_eval
128
+ return can!(*[permissions, options].flatten)
129
+ elsif setter
130
+ class_eval <<-end_eval
131
+ def #{method}(scope = nil)
132
+ can!(#{[permissions, args].flatten.join(', ').inspect}, :on => scope)
133
+ end
134
+ end_eval
135
+ return can!(*[permissions, options].flatten)
136
+ else
137
+ class_eval <<-end_eval
138
+ def #{method}(scope = nil)
139
+ can?(#{[permissions, args].flatten.join(', ').inspect}, :on => scope)
140
+ end
141
+ end_eval
142
+ return can?(*[permissions, options].flatten)
143
+ end
144
+ end
145
+ end
146
+ super
147
+ end
148
+ end
149
+ end
150
+
151
+ if defined?(ActiveRecord::Base)
152
+ ActiveRecord::Base.extend Permissive::HasPermissions::ClassMethods
153
+ end
@@ -4,40 +4,22 @@ module Permissive
4
4
  attr_writer :grant_template, :template
5
5
  belongs_to :permitted_object, :polymorphic => true
6
6
  belongs_to :scoped_object, :polymorphic => true
7
+ named_scope :granted, lambda {|permissions|
8
+ {:conditions => permissions.map{|bit| "mask & #{bit}"}.join(' AND ')}
9
+ }
7
10
  named_scope :on, lambda {|scoped_object|
8
- if scoped_object.nil?
9
- {:conditions => ['scoped_object_id IS NULL AND scoped_object_type IS NULL']}
11
+ case scoped_object
12
+ when ActiveRecord::Base
13
+ {:conditions => {:scoped_object_id => scoped_object.id, :scoped_object_type => scoped_object.class.to_s}}
14
+ when Class
15
+ {:conditions => {:scoped_object_id => nil, :scoped_object_type => scoped_object.name}}
16
+ when Symbol
17
+ {:conditions => {:scoped_object_id => nil, :scoped_object_type => scoped_object.to_s.classify}}
10
18
  else
11
- {:conditions => ['scoped_object_id = ? AND scoped_object_type = ?', scoped_object.id, scoped_object.class.to_s]}
19
+ {:conditions => {:scoped_object_id => nil, :scoped_object_type => nil}}
12
20
  end
13
21
  }
14
22
  set_table_name :permissive_permissions
15
- validates_presence_of :grant_mask, :mask, :permitted_object
16
-
17
- class << self
18
- # Use this anywhere!
19
- def bit_for(permission)
20
- Permissive::Permissions.hash[permission.to_s.downcase.to_sym] || 0
21
- end
22
- end
23
-
24
- protected
25
- def before_save
26
- # Save permission templates or "Roles"
27
- if @grant_template
28
- grant_mask = @grant_template
29
- end
30
- if @template
31
- mask = @template
32
- end
33
-
34
- # If Permissive is set to be seriously intense about who can grant what to
35
- # whom, it makes sure no bits on the grant_mask exceed those of the
36
- # permission mask
37
- # TODO: You know ... this.
38
- # if grant_mask && Permissive.strong
39
- # grant_mask = grant_mask & mask
40
- # end
41
- end
23
+ validates_presence_of :mask, :permitted_object
42
24
  end
43
25
  end
@@ -0,0 +1,94 @@
1
+ # TODO: Abstract this module more later
2
+ module Permissive
3
+ class PermissionDefinition
4
+ class << self
5
+ def define(model, options, &block)
6
+ options.assert_valid_keys(:on)
7
+ options = {:on => :global}.merge(options)
8
+
9
+ unless options[:on] == :global
10
+ options[:on] = normalize_scope(model, options[:on])
11
+ end
12
+ permission_definition = model.permissions[options[:on]] ||= Permissive::PermissionDefinition.new(model, options)
13
+ if block_given?
14
+ permission_definition.instance_eval(&block)
15
+ end
16
+ permission_definition
17
+ end
18
+
19
+ def interpolate_scope(model, scope)
20
+ attempted_scope = scope.to_s.singularize.classify
21
+ model_module = model.name.to_s.split('::')
22
+ model_module.pop
23
+ model_module = model_module.join('::')
24
+ if (model_module.blank? ? Object : Object.const_get(model_module)).const_defined?(attempted_scope)
25
+ [model_module, attempted_scope].join('::')
26
+ else
27
+ scope.to_s.classify
28
+ end
29
+ end
30
+
31
+ def normalize_scope(model, scope)
32
+ case scope
33
+ when Class
34
+ scope.name.tableize
35
+ when String, Symbol
36
+ interpolate_scope(model, scope).to_s.tableize
37
+ else
38
+ :global
39
+ end.to_s.gsub('/', '_').to_sym
40
+ end
41
+ end
42
+
43
+ def can(name, value = nil)
44
+ if value
45
+ to(name, value)
46
+ end
47
+ name = name.to_s.downcase.to_sym
48
+ roles[@role].push(name) unless roles[@role].include?(name)
49
+ end
50
+
51
+ def initialize(model, options = {})
52
+ options.assert_valid_keys(:on)
53
+ @options = options
54
+ @model = model.name
55
+ end
56
+
57
+ def model
58
+ @model.constantize
59
+ end
60
+
61
+ def on(class_name, &block)
62
+ Permissive::PermissionDefinition.define(model, @options.merge(:on => class_name), &block)
63
+ end
64
+
65
+ def permission(name, value)
66
+ unless value.is_a?(Numeric)
67
+ raise Permissive::PermissionError.new("Permissions must be integers or longs. Strings, symbols, and floats are not currently supported.")
68
+ end
69
+ permissions[name.to_s.downcase.to_sym] = 2 ** value
70
+ end
71
+ alias :to :permission
72
+
73
+ def permissions
74
+ @permissions ||= {}
75
+ end
76
+
77
+ def role(name, &block)
78
+ @role = name.to_s.to_sym
79
+ roles[@role] ||= []
80
+ instance_eval(&block)
81
+ unless model.instance_methods.include?('role=')
82
+ model.class_eval do
83
+ def role=(role_name)
84
+ self.permissions = self.class.permissions[:global].roles[role_name.to_s.downcase.to_sym]
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ def roles
91
+ @roles ||= {}
92
+ end
93
+ end
94
+ end