permissive 0.0.1 → 0.2.0.alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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