credentials 2.0.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,2 +1,3 @@
1
1
  credentials-*.gem
2
2
  coverage
3
+ doc
@@ -1,3 +1,15 @@
1
+ === 2.1.0 / 2009-11-12
2
+
3
+ * Added support for :self
4
+ * Added array arguments to +can+ and +cannot+
5
+ * Put in masses of RDoc
6
+ * Added magic method support back in
7
+
8
+ === 2.0.0 / 2009-11-11
9
+
10
+ * Rewritten from first principles! Faster, smarter, leaner and with more
11
+ features than you can shake a very light stick at.
12
+
1
13
  === 1.0.1 / 2009-04-23
2
14
 
3
15
  * Now sort of framework-agnostic. I say "sort of", because what I've actually
data/README.rdoc CHANGED
@@ -8,7 +8,7 @@ A generic actor/resource permission framework.
8
8
 
9
9
  == Installation
10
10
 
11
- * <tt>sudo gem install fauxparse-credentials --source=http://gems.github.com</tt>
11
+ * <tt>sudo gem install credentials</tt>
12
12
 
13
13
  == Examples
14
14
 
@@ -29,9 +29,6 @@ A generic actor/resource permission framework.
29
29
  b.can_edit?(b) #=> true
30
30
  b.can_edit?(a) #=> true
31
31
 
32
- Check out the example application (in +spec/test_app+) for more (completely
33
- frivolous) examples.
34
-
35
32
  == Philosophy
36
33
 
37
34
  Credentials is NOT a role-based permission system. Permissions are based
@@ -85,12 +82,8 @@ and turned into a nice pretty error page.
85
82
 
86
83
  == To do
87
84
 
88
- * The test application is largely overkill. It does a half-decent job
89
- of testing the functionality of the library, but I'd like to rewrite
90
- it into a proper test suite.
91
- * Proper documentation of the API. It's been a while since I touched this.
85
+ * Proper documentation of the API.
92
86
  * Better support for groups would make integration with RBA systems easier.
93
- * Deny permissions (+cannot+).
94
87
 
95
88
  == License
96
89
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.1.0
data/credentials.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{credentials}
8
- s.version = "2.0.0"
8
+ s.version = "2.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Matt Powell"]
12
- s.date = %q{2009-11-11}
12
+ s.date = %q{2009-11-12}
13
13
  s.description = %q{A generic actor/resource permission framework based on rules, not objects.}
14
14
  s.email = %q{fauxparse@gmail.com.com}
15
15
  s.extra_rdoc_files = [
@@ -18,7 +18,7 @@ Gem::Specification.new do |s|
18
18
  ]
19
19
  s.files = [
20
20
  ".gitignore",
21
- "History.txt",
21
+ "HISTORY",
22
22
  "LICENSE",
23
23
  "README.rdoc",
24
24
  "Rakefile",
@@ -39,6 +39,7 @@ Gem::Specification.new do |s|
39
39
  "spec/rulebook_spec.rb",
40
40
  "spec/spec_helper.rb",
41
41
  "tasks/gems.rake",
42
+ "tasks/rdoc.rake",
42
43
  "tasks/spec.rake",
43
44
  "tasks/stats.rake",
44
45
  "uninstall.rb"
@@ -1,6 +1,35 @@
1
1
  module Credentials
2
- module ObjectExtensions #:nodoc
2
+ module ObjectExtensions #:nodoc:
3
3
  module ClassMethods
4
+ # The main method for specifying and retrieving the permissions of
5
+ # a member of this class.
6
+ #
7
+ # When called with a block, this method yields a Credentials::Rulebook
8
+ # object, allowing you to specify the class's credentials
9
+ # in a declarative fashion. For example:
10
+ #
11
+ # class User
12
+ # credentials do |user|
13
+ # user.can :edit, User, :if => :administrator?
14
+ # user.can :edit, :self
15
+ # end
16
+ # end
17
+ #
18
+ # You can also specify options in this way:
19
+ #
20
+ # class User
21
+ # credentials(:default => :allow) do |user|
22
+ # user.cannot :eat, "ice cream", :if => :lactose_intolerant?
23
+ # end
24
+ # end
25
+ #
26
+ # The following options are supported:
27
+ #
28
+ # [+:default+] Whether to +:allow+ or +:deny+ permissions that aren't
29
+ # specified explicitly. The default default (!) is +:deny+.
30
+ #
31
+ # When called without a block, +credentials+ just returns the class's
32
+ # Credentials::Rulebook, creating it if necessary.
4
33
  def credentials(options = nil)
5
34
  @credentials ||= Credentials::Rulebook.new(self)
6
35
  if block_given?
@@ -12,22 +41,45 @@ module Credentials
12
41
  @credentials
13
42
  end
14
43
 
15
- def inherited_with_credentials(child_class) #:nodoc
44
+ def inherited_with_credentials(child_class) #:nodoc:
16
45
  inherited_without_credentials(child_class) if child_class.respond_to? :inherited_without_credentials
17
46
  child_class.instance_variable_set("@credentials", Rulebook.for(child_class))
18
47
  end
19
48
  end
20
49
 
21
50
  module InstanceMethods
51
+ # Returns true if the receiver has access to the specified resource or action.
22
52
  def can?(*args)
23
53
  self.class.credentials.allow? self, *args
24
54
  end
25
55
  alias_method :able_to?, :can?
56
+
57
+ # Allows you to use magic methods to test permissions.
58
+ # For example:
59
+ #
60
+ # class User
61
+ # credentials do |user|
62
+ # user.can :edit, :self
63
+ # end
64
+ # end
65
+ #
66
+ # user = User.new
67
+ # user.can_edit? user #=> true
68
+ def method_missing_with_credentials(sym, *args)
69
+ if sym.to_s =~ /\Acan_(.*)\?\z/
70
+ can? $1.to_sym, *args
71
+ else
72
+ method_missing_without_credentials sym, *args
73
+ end
74
+ end
26
75
  end
27
76
 
28
77
  def self.included(receiver) #:nodoc
29
78
  receiver.extend ClassMethods
30
79
  receiver.send :include, InstanceMethods
80
+
81
+ receiver.send :alias_method, :method_missing_without_credentials, :method_missing
82
+ receiver.send :alias_method, :method_missing, :method_missing_with_credentials
31
83
 
32
84
  class << receiver
33
85
  alias_method :inherited_without_credentials, :inherited if respond_to? :inherited
@@ -1,4 +1,8 @@
1
1
  module Credentials
2
+ # Specifies an individual 'line' in a Rulebook.
3
+ # Trivially subclassed as Credentials::Rule::AllowRule
4
+ # and Credentials::Rule::DenyRule, but this is just
5
+ # to allow you to create your own, more complex rule types.
2
6
  class Rule
3
7
  attr_accessor :parameters
4
8
  attr_accessor :options
@@ -8,15 +12,49 @@ module Credentials
8
12
  self.parameters = args
9
13
  end
10
14
 
15
+ # Returns the number of arguments expected in a test to this
16
+ # rule. This is really basic, but allows a quick first-pass
17
+ # filtering of the rules.
11
18
  def arity
12
19
  parameters.length
13
20
  end
14
21
 
22
+ # Returns +true+ if the given arguments match this rule.
23
+ # Rules mostly use the same matching criteria as Ruby's
24
+ # +case+ statement: that is, the <tt>===</tt> operator.
25
+ # Remember:
26
+ # User === User.new # a class matches an instance
27
+ # /\w+/ === "abc" # a RegExp matches a valid string
28
+ # (1..5) === 3 # a Range matches a number
29
+ # :foo === :foo # anything matches itself
30
+ #
31
+ # There are two exceptions to this behaviour. Firstly,
32
+ # if the rule specifies an array, then the argument will
33
+ # match any element of that array:
34
+ # class User
35
+ # credentials do |user|
36
+ # user.can :fight, [ :shatner, :gandhi ]
37
+ # end
38
+ # end
39
+ #
40
+ # user.can? :fight, :gandhi # => true
41
+ #
42
+ # Secondly, specifying <tt>:self</tt> in a rule is a nice
43
+ # shorthand for specifying an object's permissions on itself:
44
+ # class User
45
+ # credentials do |user|
46
+ # user.can :fight, :self # SPOILER ALERT
47
+ # end
48
+ # end
15
49
  def match?(*args)
16
50
  return false unless arity == args.length
17
51
 
18
52
  parameters.zip(args).each do |expected, actual|
19
- return false unless expected === actual
53
+ case expected
54
+ when :self then return false unless actual == args.first
55
+ when Array then return false unless expected.any? { |item| (item === actual) || (item == :self && actual == args.first) }
56
+ else return false unless expected === actual
57
+ end
20
58
  end
21
59
  result = true
22
60
  result = result && evaluate_condition(options[:if], :|, *args) unless options[:if].nil?
@@ -24,6 +62,12 @@ module Credentials
24
62
  result
25
63
  end
26
64
 
65
+ # Evaluates an +if+ or +unless+ condition.
66
+ # [+conditions+] One or more conditions to evaluate.
67
+ # Can be symbols or procs.
68
+ # [+op+] Operator used to combine the results
69
+ # (+|+ for +if+, +&+ for +unless+).
70
+ # [+args+] List of arguments to test with/against
27
71
  def evaluate_condition(conditions, op, *args)
28
72
  receiver = args.shift
29
73
  args.reject! { |arg| arg.is_a? Symbol }
@@ -3,6 +3,10 @@ require "credentials/allow_rule"
3
3
  require "credentials/deny_rule"
4
4
 
5
5
  module Credentials
6
+ # Represents a collection of rules for granting and denying
7
+ # permissions to instances of a class.
8
+ # This is the return type of a call to a class's
9
+ # +credentials+ method.
6
10
  class Rulebook
7
11
  attr_accessor :klass
8
12
  attr_accessor :options
@@ -18,6 +22,9 @@ module Credentials
18
22
  @options = {}
19
23
  end
20
24
 
25
+ # Creates a Rulebook for the given class.
26
+ # Should not be called directly: instead,
27
+ # use +class.credentials+ (q.v.).
21
28
  def self.for(klass)
22
29
  rulebook = new(klass)
23
30
  if superclass && superclass.respond_to?(:credentials)
@@ -26,22 +33,138 @@ module Credentials
26
33
  rulebook
27
34
  end
28
35
 
36
+ # Returns +true+ if there are no rules defined in this Rulebook.
29
37
  def empty?
30
38
  rules.empty?
31
39
  end
32
40
 
41
+ # Declaratively specify a permission.
42
+ # This is usually done in the context of a +credentials+ block
43
+ # (see Credentials::ObjectExtensions::ClassMethods#credentials).
44
+ # The examples below assume that context.
45
+ #
46
+ # == Simple (intransitive) permissions
47
+ # class User
48
+ # credentials do |user|
49
+ # user.can :log_in
50
+ # end
51
+ # end
52
+ # Permission is expressed as a symbol; usually an intransitive verb.
53
+ # Permission can be tested with:
54
+ # user.can? :log_in
55
+ # or
56
+ # user.can_log_in?
57
+ #
58
+ # == Resource (transitive) permissions
59
+ # class User
60
+ # credentials do |user|
61
+ # user.can :edit, Post
62
+ # end
63
+ # end
64
+ # As above, but a resource type is specified.
65
+ # Permission can be tested with:
66
+ # user.can? :edit, Post.first
67
+ # or
68
+ # user.can_edit? Post.first
69
+ #
70
+ # == +if+ and +unless+
71
+ # You can specify complex conditions with the +if+ and +unless+ options.
72
+ # These options can be either a symbol (which is assumed to be a method
73
+ # of the instance under test), or a proc, which is passed any non-symbol
74
+ # arguments from the +can?+ method.
75
+ # class User
76
+ # credentials do |user|
77
+ # user.can :create, Post, :if => :administrator?
78
+ # user.can :edit, Post, :if => lambda { |user, post| user == post.author }
79
+ # end
80
+ # end
81
+ #
82
+ # user.can? :create, Post # checks user.administrator?
83
+ # user.can? :edit, post # checks user == post.author
84
+ #
85
+ # Both +if+ and +unless+ options can be specified for the same rule:
86
+ # class User
87
+ # credentials do |user|
88
+ # user.can :eat, "chunky bacon", :if => :hungry?, :unless => :vegetarian?
89
+ # end
90
+ # end
91
+ # So, only hungry users who are not vegetarian can eat chunky bacon.
92
+ #
93
+ # You can also specify multiple options for +if+ and +unless+.
94
+ # If there are multiple options for +if+, any one match will do:
95
+ # class User
96
+ # credentials do |user|
97
+ # user.can :go_backstage, :if => [ :crew?, :good_looking? ]
98
+ # end
99
+ # end
100
+ #
101
+ # However, multiple options for +unless+ must _all_ match:
102
+ # class User
103
+ # credentials(:default => :allow) do |user|
104
+ # user.cannot :record, Album, :unless => [ :talented?, :dedicated? ]
105
+ # end
106
+ # end
107
+ # You cannot record an album unless you are both talented and dedicated.
108
+ # Note that we have specified the default permission as +allow+ in this example:
109
+ # otherwise, the rule would never match.
110
+ #
111
+ # If your rules are any more complicated than that, you might want to
112
+ # consider using the lambda form of arguments to +if+ and/or +unless+.
113
+ #
114
+ # == Reflexive permissions (+:self+)
115
+ # The following two permissions are identical:
116
+ # class User
117
+ # credentials do |user|
118
+ # user.can :edit, User, :if => lambda { |user, another| user == another }
119
+ # user.can :edit, :self
120
+ # end
121
+ # end
33
122
  def can(*args)
34
123
  self.rules << AllowRule.new(klass, *args)
35
124
  end
36
125
 
126
+ # Declaratively remove a permission.
127
+ # This is handy to explicitly remove a permission in a child class that
128
+ # you have granted in a parent class.
129
+ # It is also useful if your default is set to +allow+ and you want to
130
+ # tighten up some permissions:
131
+ # class User
132
+ # credentials(:default => :allow) do |user|
133
+ # user.cannot :delete, :self
134
+ # end
135
+ # end
136
+ # See Credentials::Rulebook#can for more on specifying permissions:
137
+ # just remember that everything is backwards!
37
138
  def cannot(*args)
38
139
  self.rules << DenyRule.new(klass, *args)
39
140
  end
40
141
 
142
+ # Determines whether to +:allow+ or +:deny+ by default.
41
143
  def default
42
144
  options[:default] && options[:default].to_sym
43
145
  end
44
146
 
147
+ # Decides whether to allow the requested permission.
148
+ #
149
+ # == Match algorithm
150
+ # 1. Set +ALLOWED+ to true if permission is specifically allowed
151
+ # by any allow_rules; otherwise, false.
152
+ # 2. Set +DENIED+ to true if permission is specifically denied
153
+ # by any deny_rules; otherwise, false.
154
+ # 3. The final result depends on the value of +default+:
155
+ # a. if +:allow+: <tt>ALLOWED OR !DENIED</tt>
156
+ # b. if +:deny+: <tt>ALLOWED AND !DENIED</tt>
157
+ #
158
+ # Expressed as a table:
159
+ # <table>
160
+ # <thead><tr><th>Matching rules</th><th>Default allow</th><th>Default deny</th></tr></thead>
161
+ # <tbody>
162
+ # <tr><td>None of the +allow+ or +deny+ rules matched.</td><td>allow</td><td>deny</td></tr>
163
+ # <tr><td>Some of the +allow+ rules matched, none of the +deny+ rules matched.</td><td>allow</td><td>allow</td></tr>
164
+ # <tr><td>None of the +allow+ rules matched, some of the +deny+ rules matched.</td><td>deny</td><td>deny</td></tr>
165
+ # <tr><td>Some of the +allow+ rules matched, some of the +deny+ rules matched.</td><td>allow</td><td>deny</td></tr>
166
+ # </tbody>
167
+ # </table>
45
168
  def allow?(*args)
46
169
  allowed = allow_rules.inject(false) { |memo, rule| memo || rule.allow?(*args) }
47
170
  denied = deny_rules.inject(false) { |memo, rule| memo || rule.deny?(*args) }
@@ -53,10 +176,12 @@ module Credentials
53
176
  end
54
177
  end
55
178
 
179
+ # Subset of rules that grant permission by exposing an +allow?+ method.
56
180
  def allow_rules
57
181
  rules.select { |rule| rule.respond_to? :allow? }
58
182
  end
59
183
 
184
+ # Subset of rules that deny permission by exposing an +deny?+ method.
60
185
  def deny_rules
61
186
  rules.select { |rule| rule.respond_to? :deny? }
62
187
  end
@@ -14,6 +14,26 @@ describe Animal do
14
14
  it "shouldn't be able to do anything without explicit permission" do
15
15
  @cow.should_not be_able_to :jump, "Moon"
16
16
  end
17
+
18
+ it "should be able to clean itself" do
19
+ @sheep.should be_able_to :clean, @sheep
20
+ end
21
+
22
+ it "should not be able to clean another animal" do
23
+ @sheep.should_not be_able_to :clean, @cow
24
+ end
25
+
26
+ it "should have magic methods for permissions" do
27
+ lambda {
28
+ @sheep.can_eat?(@cow).should == false
29
+ }.should_not raise_error
30
+ end
31
+
32
+ it "should still do normal method_missing stuff" do
33
+ lambda {
34
+ @sheep.foo
35
+ }.should raise_error
36
+ end
17
37
  end
18
38
 
19
39
  describe Carnivore do
@@ -75,6 +95,17 @@ describe Carnivore do
75
95
  end
76
96
  end
77
97
 
98
+ describe Bird do
99
+ before :each do
100
+ @toucan = Bird.new("Toucan")
101
+ @crocodile = Carnivore.new("Crocodile")
102
+ end
103
+
104
+ it "should be able to clean another animal" do
105
+ @toucan.should be_able_to :clean, @crocodile
106
+ end
107
+ end
108
+
78
109
  describe Man do
79
110
  before :each do
80
111
  @man = Man.new
data/spec/domain.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  class Animal < Struct.new(:species, :hungry, :fast)
2
2
  credentials do |animal|
3
+ animal.can :clean, :self
3
4
  end
4
5
 
5
6
  def edible?
@@ -22,7 +23,9 @@ class Prey < Animal
22
23
  end
23
24
 
24
25
  class Bird < Prey
25
-
26
+ credentials do |bird|
27
+ bird.can :clean, Animal # they are good like that
28
+ end
26
29
  end
27
30
 
28
31
  class Carnivore < Animal
@@ -35,7 +38,6 @@ end
35
38
 
36
39
  class Man < Animal
37
40
  credentials :default => :allow do |man|
38
-
39
41
  end
40
42
  end
41
43
 
data/spec/rule_spec.rb CHANGED
@@ -23,6 +23,14 @@ describe Credentials::AllowRule do
23
23
  rule.match?(lion, :jump)
24
24
  }.should raise_error(ArgumentError)
25
25
  end
26
+
27
+ it "should match with an array" do
28
+ rule = Credentials::AllowRule.new Animal, :bite, [ :self, Prey ]
29
+ lion = Carnivore.new("lion")
30
+ antelope = Prey.new("antelope")
31
+ rule.should be_match(lion, :bite, antelope)
32
+ rule.should be_match(lion, :bite, lion)
33
+ end
26
34
  end
27
35
 
28
36
  describe Credentials::DenyRule do
@@ -1,10 +1,12 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper'
2
2
 
3
3
  describe Credentials::Rulebook do
4
+ it "should be empty when created" do
5
+ Credentials::Rulebook.new(Animal).should be_empty
6
+ end
7
+
4
8
  it "should duplicate on inheritance" do
5
9
  Animal.credentials.should_not == Carnivore.credentials
6
10
  Animal.credentials.rules.should_not == Carnivore.credentials.rules
7
- Animal.credentials.should be_empty
8
- Carnivore.credentials.should_not be_empty
9
11
  end
10
12
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  $: << File.dirname(__FILE__) + "/../lib"
2
2
  require "credentials"
3
3
  require "spec"
4
+ require "date"
4
5
 
5
6
  require File.join(File.dirname(__FILE__), "domain.rb")
data/tasks/rdoc.rake ADDED
@@ -0,0 +1,17 @@
1
+ begin
2
+ require 'hanna/rdoctask'
3
+ rescue
4
+ require 'rake/rdoctask'
5
+ end
6
+
7
+ desc 'Generate RDoc documentation.'
8
+ Rake::RDocTask.new(:rdoc) do |rdoc|
9
+ rdoc.rdoc_files.include('README.rdoc', 'LICENSE', 'HISTORY').
10
+ include('lib/**/*.rb')
11
+
12
+ rdoc.main = "README.rdoc" # page to start on
13
+ rdoc.title = "credentials documentation"
14
+
15
+ rdoc.rdoc_dir = 'doc' # rdoc output folder
16
+ rdoc.options << '--webcvs=http://github.com/fauxparse/credentials/'
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: credentials
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Powell
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-11-11 00:00:00 +13:00
12
+ date: 2009-11-12 00:00:00 +13:00
13
13
  default_executable:
14
14
  dependencies: []
15
15
 
@@ -24,7 +24,7 @@ extra_rdoc_files:
24
24
  - README.rdoc
25
25
  files:
26
26
  - .gitignore
27
- - History.txt
27
+ - HISTORY
28
28
  - LICENSE
29
29
  - README.rdoc
30
30
  - Rakefile
@@ -45,6 +45,7 @@ files:
45
45
  - spec/rulebook_spec.rb
46
46
  - spec/spec_helper.rb
47
47
  - tasks/gems.rake
48
+ - tasks/rdoc.rake
48
49
  - tasks/spec.rake
49
50
  - tasks/stats.rake
50
51
  - uninstall.rb