stasi 0.0.1.alpha → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile.lock CHANGED
@@ -1,8 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- stasi (0.0.1.alpha)
5
- activesupport (>= 4.0.0)
4
+ stasi (0.1.0)
6
5
  dsl_eval (>= 0.0.2)
7
6
 
8
7
  GEM
@@ -31,6 +30,7 @@ PLATFORMS
31
30
  java
32
31
 
33
32
  DEPENDENCIES
33
+ activesupport (>= 4.0.0)
34
34
  bundler (~> 1.3)
35
35
  minitest (~> 4.2)
36
36
  rake
data/README.md CHANGED
@@ -17,30 +17,52 @@ include Robotnik::Authorization::Watch
17
17
  Then you can check permissions this way :
18
18
 
19
19
  ```ruby
20
- user.can? :read, :posts
20
+ user.can? :read, @post
21
21
  ```
22
22
 
23
23
  You define permissions in an initializer :
24
24
 
25
25
  ```ruby
26
26
  Robotnik::Authorization::Law.define do
27
+
28
+ default do
29
+ can :read, Post
30
+ end
27
31
 
28
32
  status :admin do
29
- can read, :posts
30
- can :destroy, :posts
33
+ can :edit, Post, if: Proc.new{ |post| post.editable }
34
+ can :destroy, Post
31
35
  end
32
36
 
33
37
  status :guest do
34
- can :read, :post
38
+ can :comment, :commentable
35
39
  end
36
40
 
37
41
  end
38
42
  ```
39
43
 
40
44
  Undefined permissions default to `false`.
41
- `:admin` and `:guest`, in this example, must be methods on the `user` object.
45
+ `:admin` and `:guest`, in this example, must be method names on the `user` object. The only method name that is not allowed is `:default`, as `status :default` is equivalent to `default`.
46
+
47
+ The `can` method takes two arguments : an action name as a symbol, and a resource. The resource can be :
48
+
49
+ * a class, eg. `Post`
50
+ * a symbol, eg. `:commentable`. The authorization will be applied if `@post.commentable` returns `true`. This method can take one argument, in which case, the user object will be passed to it.
51
+
52
+ Optionnally, the `can` method can take a hash with conditions (hash keys can be `if` and `unless`, values can be Proc. The resource tested will be yielded).
53
+ Finally, the `can` method can take a block, in which case the `can?` method will return the return value of the block. This is useful when defining abilities on collections :
54
+
55
+ ```ruby
56
+ can :index, Post do |posts|
57
+ posts.where(published: true)
58
+ end
59
+ ```
60
+
61
+ The `cannot` method takes only two arguments : the action name, and the resource.
42
62
 
43
63
  ## Milestones
44
64
 
45
- * pass directly object, and not a symbol
65
+ * yield user to blocks and procs in defining abilities
66
+ * pass symbol or proc to `:if` and `:unless` conditions
67
+ * alias actions :manage, :all, :read => [:index, :show], :create => [:new, :create], …
46
68
  * load specific permissions from db
data/lib/stasi.rb CHANGED
@@ -1,6 +1,4 @@
1
1
  require 'dsl_eval'
2
- require 'active_support/core_ext/hash'
3
- require 'active_support/core_ext/class/attribute_accessors'
4
2
 
5
3
  require 'stasi/authorization/law'
6
4
  require 'stasi/authorization/status'
@@ -5,8 +5,6 @@ module Robotnik
5
5
  #--- Class methods ---#
6
6
 
7
7
  include Robotnik::DslEval
8
-
9
- cattr_reader :law
10
8
 
11
9
  def self.define &block
12
10
  @@law = self.new.evaluate(&block)
@@ -16,23 +14,31 @@ module Robotnik
16
14
  @@law = nil
17
15
  end
18
16
 
17
+ def self.law
18
+ @@law
19
+ end
20
+
19
21
  #--- Instance methods ---#
20
22
 
23
+ attr_reader :statuses
24
+
25
+ def initialize
26
+ @statuses = []
27
+ end
28
+
21
29
  def status method, &block
22
30
  init_role_for(method).evaluate &block
23
31
  end
24
32
 
25
- def statuses
26
- rules.keys
33
+ def default &block
34
+ status :default, &block
27
35
  end
28
36
 
29
- def can? *args
30
- subject, args = args.shift, args
37
+ def can? action, resource, options
31
38
  verdict = false
32
39
  statuses.each do |status|
33
- if subject.send status
34
- verdict = rules[status].can? *args
35
- break
40
+ if status == :default || status.to_proc.call(options[:agent])
41
+ verdict = rules[status].can? action.to_sym, resource, options
36
42
  end
37
43
  end
38
44
  verdict
@@ -41,11 +47,14 @@ module Robotnik
41
47
  private
42
48
 
43
49
  def rules
44
- @rules ||= {}.with_indifferent_access
50
+ @rules ||= {}
45
51
  end
46
52
 
47
53
  def init_role_for method
48
- rules[method] ||= Robotnik::Authorization::Status.new
54
+ rules[method] ||= begin
55
+ @statuses << method
56
+ Robotnik::Authorization::Status.new
57
+ end
49
58
  end
50
59
 
51
60
  end
@@ -14,31 +14,45 @@ module Robotnik
14
14
  rules[resource][action] = false
15
15
  end
16
16
 
17
- def can? action, resource, subject
18
- begin
19
- condition = rules[resource][action]
20
- case condition
21
- when true, false then condition
22
- else
23
- return condition.call(subject) if condition.respond_to?(:call)
24
- deliberation = true
25
- deliberation = deliberation && condition[:if].call(subject) if condition.has_key?(:if)
26
- deliberation = deliberation && (! condition[:unless].call(subject)) if deliberation && condition.has_key?(:unless)
27
- deliberation
17
+ def can? action, resource, options={}
18
+ verdict = false
19
+ rules.each do |rule_condition, actions|
20
+ if Robotnik::Authorization::Status.matches? rule_condition, resource, options
21
+ action_condition = actions[action]
22
+ verdict = case action_condition
23
+ when true, false then action_condition
24
+ else
25
+ if action_condition.respond_to?(:call)
26
+ action_condition.call(resource)
27
+ else
28
+ deliberation = true
29
+ deliberation = deliberation && action_condition[:if].call(resource) if action_condition.has_key?(:if)
30
+ deliberation = deliberation && (! action_condition[:unless].call(resource)) if deliberation && action_condition.has_key?(:unless)
31
+ deliberation
32
+ end
33
+ end
28
34
  end
29
- rescue NoMethodError
30
- false
35
+ end
36
+ verdict
37
+ end
38
+
39
+ def self.matches? rule_condition, resource, options
40
+ rule_condition = rule_condition.to_proc if rule_condition.respond_to?(:to_proc)
41
+ begin
42
+ rule_condition === resource
43
+ rescue ArgumentError
44
+ rule_condition.call(resource, options[:agent])
31
45
  end
32
46
  end
33
47
 
34
48
  private
35
49
 
36
50
  def rules
37
- @rules ||= {}.with_indifferent_access
51
+ @rules ||= {}
38
52
  end
39
53
 
40
54
  def init_rule_for resource
41
- rules[resource] ||= {}.with_indifferent_access
55
+ rules[resource] ||= {}
42
56
  end
43
57
 
44
58
  end
@@ -3,7 +3,9 @@ module Robotnik
3
3
  module Watch
4
4
 
5
5
  def can? *args
6
- Robotnik::Authorization::Law.law.can? *(args.unshift(self))
6
+ args[2] ||= {}
7
+ args[2][:agent] = self
8
+ Robotnik::Authorization::Law.law.can? *args
7
9
  end
8
10
 
9
11
  end
data/test/law_test.rb CHANGED
@@ -8,7 +8,7 @@ class LawTest < ActiveSupport::TestCase
8
8
 
9
9
  test "it has rules" do
10
10
  rules = @law.send :rules
11
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, rules
11
+ assert_instance_of Hash, rules
12
12
  end
13
13
 
14
14
  test "it initiates rules for role" do
@@ -20,20 +20,20 @@ class LawTest < ActiveSupport::TestCase
20
20
 
21
21
  test "it yields status for authorization definition" do
22
22
  @law.status :admin do
23
- can :read, :book
23
+ can :read, Object
24
24
  end
25
- assert @law.instance_variable_get('@rules')[:admin].can? :read, :book, nil
25
+ assert @law.instance_variable_get('@rules')[:admin].can? :read, Object.new
26
26
  end
27
27
 
28
28
  test "it has statuses" do
29
29
  @law.status :admin do
30
30
  end
31
- assert_equal ['admin'], @law.statuses
31
+ assert_equal [:admin], @law.statuses
32
32
  end
33
33
 
34
34
  test "it tests status on given object" do
35
- object = Object.new
36
- class << object
35
+ user = Object.new
36
+ class << user
37
37
  def admin
38
38
  true
39
39
  end
@@ -42,13 +42,13 @@ class LawTest < ActiveSupport::TestCase
42
42
  end
43
43
  end
44
44
  @law.status :admin do
45
- can :read, :book
45
+ can :read, Object
46
46
  end
47
47
  @law.status :not_admin do
48
- cannot :read, :book
48
+ cannot :read, Object
49
49
  end
50
- assert @law.can? object, :read, :book, nil
51
- class << object
50
+ assert @law.can? :read, Object.new, {agent: user}
51
+ class << user
52
52
  def admin
53
53
  false
54
54
  end
@@ -56,19 +56,19 @@ class LawTest < ActiveSupport::TestCase
56
56
  true
57
57
  end
58
58
  end
59
- refute @law.can? object, :read, :book, nil
59
+ refute @law.can? :read, Object.new, {agent: user}
60
60
  end
61
61
 
62
62
  test "it returns false if no status returns true" do
63
- object = Object.new
64
- class << object
63
+ user = Object.new
64
+ class << user
65
65
  def admin
66
66
  false
67
67
  end
68
68
  end
69
69
  @law.status :admin do
70
70
  end
71
- refute @law.can? object, :do, :something, nil
71
+ refute @law.can? :do, :something, {agent: user}
72
72
  end
73
73
 
74
74
  test "it defines a new law at class level" do
@@ -76,7 +76,33 @@ class LawTest < ActiveSupport::TestCase
76
76
  status :admin do
77
77
  end
78
78
  end
79
- assert_equal ['admin'], Robotnik::Authorization::Law.law.statuses
79
+ assert_equal [:admin], Robotnik::Authorization::Law.law.statuses
80
+ end
81
+
82
+ test "it parses all stauses" do
83
+ user = Object.new
84
+ class << user
85
+ def is_a_a
86
+ true
87
+ end
88
+ def is_a_b
89
+ true
90
+ end
91
+ end
92
+ @law.status :is_a_a do
93
+ can :read, Object
94
+ end
95
+ @law.status :is_a_b do
96
+ cannot :read, Object
97
+ end
98
+ refute @law.can? :read, Object.new, {agent: user}
99
+ end
100
+
101
+ test "it accepts defaults" do
102
+ @law.default do
103
+ can :read, Object
104
+ end
105
+ assert @law.can? :read, Object.new, {agent: Object.new}
80
106
  end
81
107
 
82
108
  end
data/test/status_test.rb CHANGED
@@ -2,59 +2,113 @@ require 'test_helper'
2
2
 
3
3
  class StatusTest < ActiveSupport::TestCase
4
4
 
5
+ Book = Class.new
6
+
5
7
  def setup
6
8
  @status = Robotnik::Authorization::Status.new
7
9
  end
8
10
 
9
11
  test "it has rules" do
10
12
  rules = @status.send :rules
11
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, rules
13
+ assert_instance_of Hash, rules
12
14
  end
13
15
 
14
16
  test "it initiates rules for resource" do
15
17
  @status.send :init_rule_for, :resource
16
18
  rules = @status.instance_variable_get('@rules')
17
19
  assert rules.has_key? :resource
18
- assert_instance_of ActiveSupport::HashWithIndifferentAccess, rules[:resource]
20
+ assert_instance_of Hash, rules[:resource]
19
21
  end
20
22
 
21
23
  test "it defines prohibition" do
22
- @status.cannot :read, :book
23
- refute @status.can? :read, :book, nil
24
+ @status.cannot :read, Book
25
+ refute @status.can? :read, Book.new
26
+ assert_equal false, @status.instance_variable_get('@rules')[Book][:read]
24
27
  end
25
28
 
26
29
  test "it defines strict authorization" do
27
- @status.can :read, :book
28
- assert @status.can? :read, :book, nil
30
+ @status.can :read, Book
31
+ assert @status.can? :read, Book.new
32
+ assert_equal true, @status.instance_variable_get('@rules')[Book][:read]
29
33
  end
30
34
 
31
35
  test "it defines authorization with if and unless options" do
36
+ Post = Struct.new :name
32
37
  assertions = [true, false, false, true, false, true, false, false]
33
38
  [[true, nil], [nil, true], [false, nil], [nil, false], [true, true], [true, false], [false, true], [false, false]].each_with_index do |conditions, i|
34
39
  conditions_hash = {}
35
40
  [:if, :unless].each_with_index do |operator, j|
36
41
  unless conditions[j].nil?
37
42
  if conditions[j]
38
- conditions_hash[operator] = Proc.new{ |num| num == 1 }
43
+ conditions_hash[operator] = Proc.new{ |post| post.name == 'test' }
39
44
  else
40
- conditions_hash[operator] = Proc.new{ |num| num != 1 }
45
+ conditions_hash[operator] = Proc.new{ |post| post.name != 'test' }
41
46
  end
42
47
  end
43
48
  end
44
- @status.can :read, :book, conditions_hash
45
- assert_equal assertions[i], @status.can?(:read, :book, 1)
49
+ @status.can :read, Post, conditions_hash
50
+ assert_equal assertions[i], @status.can?(:read, Post.new('test'))
46
51
  end
47
52
  end
48
53
 
49
54
  test "it defines authorizations with a block" do
50
- @status.can :read, :book do |num|
51
- num * 2
55
+ Book.class_eval do
56
+ attr_accessor :collection
57
+ def self.all
58
+ Book.new.tap do |book|
59
+ book.collection = [1, 2, 3, 4]
60
+ end
61
+ end
52
62
  end
53
- assert_equal 6, @status.can?(:read, :book, 3)
63
+ @status.can :read, Book do |books|
64
+ books.collection.first 2
65
+ end
66
+ assert_equal [1, 2], @status.can?(:read, Book.all)
54
67
  end
55
68
 
56
69
  test "it defaults to forbidden if permission is not defined" do
57
- refute @status.can? :be, :free, :nil
58
- end
70
+ refute @status.can? :be, :free
71
+ end
72
+
73
+ test "it accepts a class" do
74
+ @status.can :read, Object
75
+ assert @status.can? :read, Object.new
76
+ assert @status.can? :read, Object
77
+ end
78
+
79
+ test "it accepts a symbol as a method name" do
80
+ o = Object.new
81
+ class << o
82
+ def taggable
83
+ true
84
+ end
85
+ end
86
+ @status.can :read, :taggable
87
+ assert @status.can? :read, o
88
+ end
89
+
90
+ test "it accepts a symbol for a method with one argument and evaluates it with the agent" do
91
+ o = Object.new
92
+ class << o
93
+ def taggable user
94
+ user[:name] == 'test'
95
+ end
96
+ end
97
+ @status.can :read, :taggable
98
+ assert @status.can? :read, o, {agent: {name: 'test'}}
99
+ refute @status.can? :read, o, {agent: {name: 'tested'}}
100
+ end
101
+
102
+ test "the last matching condition is returned" do
103
+ o = Object.new
104
+ class << o
105
+ def taggable
106
+ true
107
+ end
108
+ end
109
+ @status.cannot :read, Object
110
+ @status.can :read, :taggable
111
+ assert @status.can? :read, o
112
+ end
59
113
 
60
114
  end
data/test/test_helper.rb CHANGED
@@ -5,5 +5,4 @@ require 'minitest/unit'
5
5
  require 'active_support/test_case'
6
6
  require 'minitest/autorun'
7
7
  require 'turn'
8
- require 'turn/colorize'
9
8
  require 'turn/autorun'
data/test/watch_test.rb CHANGED
@@ -2,20 +2,8 @@ require 'test_helper'
2
2
 
3
3
  class WatchTest < ActiveSupport::TestCase
4
4
 
5
- test "calls the law class can? with self as first arg" do
6
- object = Object.new
7
- object.extend Robotnik::Authorization::Watch
8
- class << object
9
- def admin
10
- true
11
- end
12
- end
13
- Robotnik::Authorization::Law.define do
14
- status :admin do
15
- can :do, :something
16
- end
17
- end
18
- assert object.can?(:do, :something, :stupid)
5
+ test "passes the watched agent in the options hash" do
6
+ skip "could not figure out how to use assert_send"
19
7
  end
20
8
 
21
9
  end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stasi
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: 6
5
- version: 0.0.1.alpha
4
+ prerelease:
5
+ version: 0.1.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - Paul Vonderscher
@@ -76,34 +76,34 @@ dependencies:
76
76
  prerelease: false
77
77
  type: :development
78
78
  - !ruby/object:Gem::Dependency
79
- name: dsl_eval
79
+ name: activesupport
80
80
  version_requirements: !ruby/object:Gem::Requirement
81
81
  requirements:
82
82
  - - '>='
83
83
  - !ruby/object:Gem::Version
84
- version: 0.0.2
84
+ version: 4.0.0
85
85
  none: false
86
86
  requirement: !ruby/object:Gem::Requirement
87
87
  requirements:
88
88
  - - '>='
89
89
  - !ruby/object:Gem::Version
90
- version: 0.0.2
90
+ version: 4.0.0
91
91
  none: false
92
92
  prerelease: false
93
- type: :runtime
93
+ type: :development
94
94
  - !ruby/object:Gem::Dependency
95
- name: activesupport
95
+ name: dsl_eval
96
96
  version_requirements: !ruby/object:Gem::Requirement
97
97
  requirements:
98
98
  - - '>='
99
99
  - !ruby/object:Gem::Version
100
- version: 4.0.0
100
+ version: 0.0.2
101
101
  none: false
102
102
  requirement: !ruby/object:Gem::Requirement
103
103
  requirements:
104
104
  - - '>='
105
105
  - !ruby/object:Gem::Version
106
- version: 4.0.0
106
+ version: 0.0.2
107
107
  none: false
108
108
  prerelease: false
109
109
  type: :runtime
@@ -144,9 +144,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
144
144
  none: false
145
145
  required_rubygems_version: !ruby/object:Gem::Requirement
146
146
  requirements:
147
- - - '>'
147
+ - - '>='
148
148
  - !ruby/object:Gem::Version
149
- version: 1.3.1
149
+ version: '0'
150
150
  none: false
151
151
  requirements: []
152
152
  rubyforge_project: