credentials 2.0.0 → 2.1.0

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/.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