stasi 0.0.1.alpha → 0.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/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: