kan 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 4b05a0ee35f96b6d7cf1860821bbb9377ce6865d
4
- data.tar.gz: 2c95160455b028aeddc5888a39659f61a2fef75e
2
+ SHA256:
3
+ metadata.gz: 7905462f6cf7046f2348d0fd4a72eeaa8388d65ca37e82d3b1817adcb8b0b5c5
4
+ data.tar.gz: 1e55a1e1b765f290110270a3ecf49fc9335979701030b3273f2ac155fb6918cc
5
5
  SHA512:
6
- metadata.gz: e2d8dc36dfdfebf3da5f453824cc94b46f478346963d10d1d92c23873045cb2efb94debd260d5192e3233931236d09e80ede00b60e75e61b88b189e34fa55dc1
7
- data.tar.gz: bc672426e146ad132e7339dd780c171e8da22ce350b70d9fc54a6bd1c1f9a5505faa64f5009c01d20036fab86ab37f19396b034314c2f4bb92d7f98aa76702c9
6
+ metadata.gz: 0e431fe2a8833613fcfdcf5735c33a42e8627fa7ec57bb22a1560a5a4cfeae5b048c44b3adec766fff2c8db09ebf599654d2d84179301479b3ae8d7a11dc41d9
7
+ data.tar.gz: e2e3396a2f552d3c326a09cd707c1fa9c40cd92ce7e171e718a1f61f7f810fe804c9f25061673d2d0de8f5b32003e2920e58769972019ba5e287e2adba18df96
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ .rubocop-https-*
@@ -0,0 +1,15 @@
1
+ # Please keep AllCops, Bundler, Style, Metrics groups and then order cops
2
+ # alphabetically
3
+ inherit_from:
4
+ - https://raw.githubusercontent.com/hanami/devtools/master/.rubocop.yml
5
+ AllCops:
6
+ Exclude:
7
+ - "examples/**/*"
8
+ - "vendor/**/*"
9
+ - "spec/support/**/*"
10
+ - "**/*.gemspec"
11
+ Style/IndentHeredoc:
12
+ Enabled: true
13
+ Style/MixinGrouping:
14
+ Exclude:
15
+ - "spec/support/**/*"
@@ -1,5 +1,17 @@
1
1
  ## HEAD
2
2
 
3
+ * Add rubocop to the project (@davydovanton)
4
+ * Raise error if application take invalid scope (@davydovanton) #31
5
+ * Raise error if user try to register `roles` ability (@davydovanton) #30
6
+ * Allow to detect roles for abilities object and scope (@davydovanton) #28
7
+ * New documentation page (@davydovanton) #21
8
+
9
+ ## v0.3
10
+
11
+ * Allow to use callable objects as a role objects (@davydovanton) #20
12
+ * Allow to use classes as a role objects (@davydovanton) #18
13
+ * Add `#permit` matcher for rspec specs (@berniechiu) #15
14
+
3
15
  ## v0.2
4
16
 
5
17
  * Add logger support (@valikos) #7
data/Gemfile CHANGED
@@ -1,6 +1,8 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in kan.gemspec
6
6
  gemspec
7
+
8
+ gem 'rubocop', '0.50.0', require: false
@@ -1,11 +1,12 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- kan (0.2.0)
4
+ kan (0.4.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
+ ast (2.4.0)
9
10
  concurrent-ruby (1.0.5)
10
11
  diff-lcs (1.3)
11
12
  dry-auto_inject (0.4.5)
@@ -15,6 +16,12 @@ GEM
15
16
  dry-container (0.6.0)
16
17
  concurrent-ruby (~> 1.0)
17
18
  dry-configurable (~> 0.1, >= 0.1.3)
19
+ parallel (1.12.1)
20
+ parser (2.5.1.0)
21
+ ast (~> 2.4.0)
22
+ powerpack (0.1.2)
23
+ rainbow (2.2.2)
24
+ rake
18
25
  rake (10.5.0)
19
26
  rspec (3.7.0)
20
27
  rspec-core (~> 3.7.0)
@@ -29,6 +36,15 @@ GEM
29
36
  diff-lcs (>= 1.2.0, < 2.0)
30
37
  rspec-support (~> 3.7.0)
31
38
  rspec-support (3.7.0)
39
+ rubocop (0.50.0)
40
+ parallel (~> 1.10)
41
+ parser (>= 2.3.3.1, < 3.0)
42
+ powerpack (~> 0.1)
43
+ rainbow (>= 2.2.2, < 3.0)
44
+ ruby-progressbar (~> 1.7)
45
+ unicode-display_width (~> 1.0, >= 1.0.1)
46
+ ruby-progressbar (1.9.0)
47
+ unicode-display_width (1.4.0)
32
48
 
33
49
  PLATFORMS
34
50
  ruby
@@ -39,6 +55,7 @@ DEPENDENCIES
39
55
  kan!
40
56
  rake (~> 10.0)
41
57
  rspec (~> 3.7)
58
+ rubocop (= 0.50.0)
42
59
 
43
60
  BUNDLED WITH
44
61
  1.16.1
data/README.md CHANGED
@@ -7,12 +7,6 @@ Simple functional authorization library for ruby. Inspired by [transproc](https:
7
7
 
8
8
  * [Installation](#installation)
9
9
  * [Usage](#usage)
10
- * [Register abilities](#register-abilities)
11
- * [Check abilities](#check-abilities)
12
- * [Default ability block](#default-ability-block)
13
- * [List of abilities](#list-of-abilities)
14
- * [Roles](#roles)
15
- * [Dry-auto\_inject](#dry-auto_inject)
16
10
  * [Contributing](#contributing)
17
11
  * [License](#license)
18
12
  * [Code of Conduct](#code-of-conduct)
@@ -35,15 +29,25 @@ Or install it yourself as:
35
29
 
36
30
  ## Usage
37
31
 
32
+ See [User Documentation page](https://blog.davydovanton.com/kan/)
33
+
34
+ * [Base Usage](https://blog.davydovanton.com/kan/)
35
+ * [Roles](https://blog.davydovanton.com/kan/roles)
36
+ * [Testing](https://blog.davydovanton.com/kan/testing)
37
+ * [Dry integration](https://blog.davydovanton.com/kan/working_with_dry)
38
+ * [F.A.Q.]()https://blog.davydovanton.com/kan/faq
39
+
40
+ ## Basic Usage
41
+
38
42
  ### Register abilities
39
43
 
40
44
  ```ruby
41
45
  class Post::Abilities
42
46
  include Kan::Abilities
43
47
 
44
- register 'read' { |_, _| true }
45
- register 'edit' { |user, post| user.id == post.user_id }
46
- register 'delete' { |_, _| false }
48
+ register('read') { |_, _| true }
49
+ register('edit') { |user, post| user.id == post.user_id }
50
+ register('delete') { |_, _| false }
47
51
  end
48
52
  ```
49
53
 
@@ -53,16 +57,16 @@ Also, you can register more than one ability in one place and use string or symb
53
57
  class Post::AdminAbilities
54
58
  include Kan::Abilities
55
59
 
56
- register :read, :edit, :delete { |user, _| user.admin? }
60
+ register(:read, :edit, :delete) { |user, _| user.admin? }
57
61
  end
58
62
 
59
63
  class Comments::Abilities
60
64
  include Kan::Abilities
61
65
 
62
- register 'read' { |_, _| true }
63
- register 'edit' { |user, _| user.admin? }
66
+ register('read') { |_, _| true }
67
+ register('edit') { |user, _| user.admin? }
64
68
 
65
- register :delete do |user, comment|
69
+ register(:delete) do |user, comment|
66
70
  user.id == comment.user_id && comment.created_at < Time.now + TEN_MINUTES
67
71
  end
68
72
  end
@@ -116,123 +120,15 @@ global_abilities['post.edit'].call(owner_user, post) # => true
116
120
  global_abilities['post.edit'].call(admin_user, post) # => true
117
121
  ```
118
122
 
119
- ### Roles
120
- Kan provide simple role system. For this you need to define role block in each abilities classes:
121
- ```ruby
122
- module Post
123
- class AnonymousAbilities
124
- include Kan::Abilities
125
-
126
- role :anonymous do |user, _|
127
- user.id.nil?
128
- end
129
-
130
- register(:read, :edit, :delete) { false }
131
- end
132
-
133
- class BaseAbilities
134
- include Kan::Abilities
135
-
136
- role :all do |_, _|
137
- true
138
- end
139
-
140
- register(:read) { |_, _| true }
141
- register(:edit, :delete) { |user, post| false }
142
- end
143
-
144
-
145
- class AuthorAbilities
146
- include Kan::Abilities
147
-
148
- role :author do |user, post|
149
- user.id == post.author_id
150
- end
151
-
152
- register(:read, :edit) { |_, _| true }
153
- register(:delete) { |_, _| false }
154
- end
155
-
156
- class AdminAbilities
157
- include Kan::Abilities
158
-
159
- role :admin do |user, _|
160
- user.admin?
161
- end
162
-
163
- register :read, :edit, :delete { |_, _| true }
164
- end
165
- end
166
- ```
167
-
168
- After that initialize Kan application object and call it with payload:
169
- ```ruby
170
- abilities = Kan::Application.new(
171
- post: [Post::AnonymousAbilities.new, Post::BaseAbilities.new, Post::AuthorAbilities.new, Post::AdminAbilities.new],
172
- comment: Comments::Abilities.new
173
- )
174
-
175
- abilities['post.read'].call(anonymous, post) # => false
176
- abilities['post.read'].call(regular, post) # => true
177
- abilities['post.read'].call(author, post) # => true
178
- abilities['post.read'].call(admin, post) # => true
179
-
180
- abilities['post.edit'].call(anonymous, post) # => false
181
- abilities['post.edit'].call(regular, post) # => false
182
- abilities['post.edit'].call(author, post) # => true
183
- abilities['post.edit'].call(admin, post) # => true
184
-
185
- abilities['post.delete'].call(anonymous, post) # => false
186
- abilities['post.delete'].call(regular, post) # => false
187
- abilities['post.delete'].call(author, post) # => false
188
- abilities['post.delete'].call(admin, post) # => true
189
- ```
190
-
191
- ### Logger support
192
- By default kan support default ruby logger (`Logger.new` class). For setup custom logger you can use `logger` option for each abilities instances:
193
- ```ruby
194
- abilities = Kan::Application.new(
195
- comment: Comments::Abilities.new(logger: MyCustomLogger.new)
196
- )
197
- ```
198
-
199
- And call it from ability block:
200
- ```ruby
201
- class AnonymousAbilities
202
- include Kan::Abilities
203
-
204
- register(:read, :edit, :delete) do
205
- logger.info 'Anonymous ability checked'
206
- false
207
- end
208
- end
209
- ```
210
-
211
- ### Dry-auto\_inject
212
- ```ruby
213
- AbilitiesImport = Dry::AutoInject(Kan::Application.new({}))
214
-
215
- # Operation
216
-
217
- class UpdateOperation
218
- include AbilitiesImport[ability_checker: 'post.edit']
219
-
220
- def call(user, params)
221
- return Left(:permission_denied) unless ability_checker.call(user)
222
- # ...
223
- end
224
- end
225
-
226
- # Specs
227
-
228
- UpdateOperation.new(ability_checker: ->(*) { true })
229
- UpdateOperation.new(ability_checker: ->(*) { false })
230
- ```
231
-
232
123
  ## Contributing
233
124
 
125
+ ### Code and features
126
+
234
127
  Bug reports and pull requests are welcome on GitHub at https://github.com/davydovanton/kan. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
235
128
 
129
+ ### Docs
130
+ Just send PR with changes in `docs/` folder.
131
+
236
132
  ### How to instal the project
237
133
  Just clone repository and call:
238
134
 
data/Rakefile CHANGED
@@ -7,6 +7,6 @@ begin
7
7
  RSpec::Core::RakeTask.new(:spec)
8
8
 
9
9
  task default: :spec
10
- rescue LoadError
10
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
11
11
  # no rspec available
12
12
  end
@@ -0,0 +1,3 @@
1
+ theme: jekyll-theme-primer
2
+ title: Kan
3
+ description: User documentation
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html lang="{{ site.lang | default: "en-US" }}">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+
8
+ {% seo %}
9
+ <link rel="stylesheet" href="{{ "/assets/css/style.css?v=" | append: site.github.build_revision | relative_url }}">
10
+ </head>
11
+ <body>
12
+ <div class="container-lg px-3 my-5 markdown-body">
13
+ {% if site.title and site.title != page.title %}
14
+ <h1><a href="{{ "/" | absolute_url }}">{{ site.title }}</a></h1>
15
+ <p><a href="http://github.com/davydovanton/kan">GitHub</a></p>
16
+ {% endif %}
17
+
18
+ <p>Simple functional authorization library and role managment for ruby. Inspired by <a href="https://github.com/solnic/transproc">transproc</a> and <a href="http://dry-rb.org">dry project</a></p>
19
+
20
+ <ul>
21
+ <li><a href="{{ "/" | absolute_url }}">Base Usage</a></li>
22
+ <li><a href="{{ "/roles" | absolute_url }}">Roles</a></li>
23
+ <li><a href="{{ "/testing" | absolute_url }}">Testing</a></li>
24
+ <li><a href="{{ "/working_with_dry" | absolute_url }}">Dry integration</a></li>
25
+ <li><a href="{{ "/faq" | absolute_url }}">F.A.Q.</a></li>
26
+ </ul>
27
+
28
+ {{ content }}
29
+
30
+ {% if site.github.private != true and site.github.license %}
31
+ <div class="footer border-top border-gray-light mt-5 pt-3 text-right text-gray">
32
+ This site is open source. {% github_edit_link "Improve this page" %}.
33
+ </div>
34
+ {% endif %}
35
+
36
+ </div>
37
+ </body>
38
+ </html>
@@ -0,0 +1,24 @@
1
+ ## F.A.Q.
2
+
3
+ > In the example "Also, you can register more than one ability in one place and use string or symbol keys:", I don't understand how these ablities are triggered -- do PostAbilities and AdminAbilities somehow both apply at once? Can you add to this example to show how you'd call the auth check, and what would be checked?
4
+
5
+ Kan will call each ability object from your register list. If all objects return `false` `abilities[name].call(payload)` will return `false` too. I.e.:
6
+
7
+ ```ruby
8
+ abilities = Kan::Application.new(
9
+ post: [Post::Abilities.new, Post::AdminAbilities.new],
10
+ comment: Comments::Abilities.new
11
+ )
12
+
13
+ # [Post::Abilities.new -> false, Post::AdminAbilities.new -> false] -> false
14
+ abilities['post.edit'].call(current_user, post)
15
+ # => false
16
+
17
+ # [Post::Abilities.new -> true, Post::AdminAbilities.new -> false] -> true
18
+ global_abilities['post.edit'].call(owner_user, post)
19
+ # => true
20
+
21
+ # [Post::Abilities.new -> false, Post::AdminAbilities.new -> true] -> true
22
+ abilities['post.edit'].call(admin_user, post)
23
+ # => true
24
+ ```
@@ -0,0 +1,81 @@
1
+ ## Basic Usage
2
+ ### Register abilities
3
+
4
+ ```ruby
5
+ class Post::Abilities
6
+ include Kan::Abilities
7
+
8
+ register('read') { |_, _| true }
9
+ register('edit') { |user, post| user.id == post.user_id }
10
+ register('delete') { |_, _| false }
11
+ end
12
+ ```
13
+
14
+ Also, you can register more than one ability in one place and use string or symbol keys:
15
+
16
+ ```ruby
17
+ class Post::AdminAbilities
18
+ include Kan::Abilities
19
+
20
+ register(:read, :edit, :delete) { |user, _| user.admin? }
21
+ end
22
+
23
+ class Comments::Abilities
24
+ include Kan::Abilities
25
+
26
+ register('read') { |_, _| true }
27
+ register('edit') { |user, _| user.admin? }
28
+
29
+ register(:delete) do |user, comment|
30
+ user.id == comment.user_id && comment.created_at < Time.now + TEN_MINUTES
31
+ end
32
+ end
33
+ ```
34
+
35
+ ### Check abilities
36
+
37
+ ```ruby
38
+ abilities = Kan::Application.new(
39
+ post: Post::Abilities.new,
40
+ comment: Comments::Abilities.new
41
+ )
42
+
43
+ abilities['post.read'].call(current_user, post) # => true
44
+ abilities['post.delete'].call(current_user, post) # => false
45
+ abilities['comment.delete'].call(current_user, post) # => false
46
+ ```
47
+
48
+ #### Default ability block
49
+
50
+ By default Kan use `proc { true }` as a default ability block:
51
+
52
+ ```ruby
53
+ abilities['comment.invalid'].call(current_user, post) # => true
54
+ ```
55
+
56
+ But you can rewrite it
57
+
58
+ ```ruby
59
+ admin_abilities = Kan::Application.new(
60
+ post: Post::AdminAbilities.new(default_ability_block: proc { false }),
61
+ comment: Comments::Abilities.new,
62
+ )
63
+
64
+ admin_abilities['post.delete'].call(current_user, post) # => false
65
+ admin_abilities['post.delete'].call(admin_user, post) # => true
66
+ admin_abilities['post.invalid'].call(current_user, post) # => false
67
+ ```
68
+
69
+ #### List of abilities
70
+ You can provide array of abilities for each scope and Kan will return `true` if at least one ability return `true`:
71
+
72
+ ```ruby
73
+ global_abilities = Kan::Application.new(
74
+ post: [Post::Abilities.new, Post::AdminAbilities.new],
75
+ comment: Comments::Abilities.new
76
+ )
77
+
78
+ global_abilities['post.edit'].call(current_user, post) # => false
79
+ global_abilities['post.edit'].call(owner_user, post) # => true
80
+ global_abilities['post.edit'].call(admin_user, post) # => true
81
+ ```
@@ -0,0 +1,178 @@
1
+ ## Roles
2
+ Kan provide simple role system. It will detect all abilities object where role is true. For this you need to define role block in each abilities classes:
3
+
4
+ ```ruby
5
+ module Post
6
+ class AnonymousAbilities
7
+ include Kan::Abilities
8
+
9
+ role(:anonymous) do |user, _|
10
+ user.id.nil?
11
+ end
12
+
13
+ register(:read, :edit, :delete) { false }
14
+ end
15
+
16
+ class BaseAbilities
17
+ include Kan::Abilities
18
+
19
+ role(:all) do |_, _|
20
+ true
21
+ end
22
+
23
+ register(:read) { |_, _| true }
24
+ register(:edit, :delete) { |user, post| false }
25
+ end
26
+
27
+
28
+ class AuthorAbilities
29
+ include Kan::Abilities
30
+
31
+ role(:author) do |user, post|
32
+ user.id == post.author_id
33
+ end
34
+
35
+ register(:read, :edit) { |_, _| true }
36
+ register(:delete) { |_, _| false }
37
+ end
38
+
39
+ class AdminAbilities
40
+ include Kan::Abilities
41
+
42
+ role(:admin) do |user, _|
43
+ user.admin?
44
+ end
45
+
46
+ register(:read, :edit, :delete) { |_, _| true }
47
+ end
48
+ end
49
+ ```
50
+
51
+ After that initialize Kan application object and call it with payload:
52
+
53
+ ```ruby
54
+ abilities = Kan::Application.new(
55
+ post: [Post::AnonymousAbilities.new, Post::BaseAbilities.new, Post::AuthorAbilities.new, Post::AdminAbilities.new],
56
+ comment: Comments::Abilities.new
57
+ )
58
+
59
+ abilities['post.read'].call(anonymous, post) # => role: true (anonymous), result: false
60
+ abilities['post.read'].call(regular, post) # => role: true (base), result: true
61
+ abilities['post.read'].call(author, post) # => role: true (author), result: true
62
+ abilities['post.read'].call(admin, post) # => role: true (admin), result: true
63
+
64
+ abilities['post.edit'].call(anonymous, post) # => false
65
+ abilities['post.edit'].call(regular, post) # => false
66
+ abilities['post.edit'].call(author, post) # => true
67
+ abilities['post.edit'].call(admin, post) # => true
68
+
69
+ abilities['post.delete'].call(anonymous, post) # => false
70
+ abilities['post.delete'].call(regular, post) # => false
71
+ abilities['post.delete'].call(author, post) # => false
72
+ abilities['post.delete'].call(admin, post) # => true
73
+ ```
74
+
75
+ ### Class objects as role
76
+
77
+ Kan allow to use classes as roles for incapulate and easily testing your roles.
78
+
79
+ ```ruby
80
+ module Post
81
+ module Roles
82
+ class Admin
83
+ def call(user, _)
84
+ user.admin?
85
+ end
86
+ end
87
+
88
+ class Anonymous
89
+ def call(user, _)
90
+ user.id.nil?
91
+ end
92
+ end
93
+ end
94
+
95
+ class AnonymousAbilities
96
+ include Kan::Abilities
97
+
98
+ role :anonymous, Anonymous
99
+
100
+ register(:read, :edit, :delete) { false }
101
+ end
102
+
103
+ class AdminAbilities
104
+ include Kan::Abilities
105
+
106
+ role :admin, Roles::Admin
107
+
108
+ register(:read, :edit, :delete) { |_, _| true }
109
+ end
110
+ end
111
+ ```
112
+
113
+ #### Callable objects as role
114
+
115
+ Kan allow to use "callable" (objects with `#call` method) as a role object. For this just put it into ability class:
116
+
117
+ ```ruby
118
+ module Post
119
+ module Roles
120
+ class Admin
121
+ def call(user, _)
122
+ user.admin?
123
+ end
124
+ end
125
+
126
+ class Anonymous
127
+ def call(user, _)
128
+ user.id.nil?
129
+ end
130
+ end
131
+ end
132
+
133
+ class AnonymousAbilities
134
+ include Kan::Abilities
135
+
136
+ role :anonymous, Roles::Anonymous.new
137
+
138
+ register(:read, :edit, :delete) { false }
139
+ end
140
+
141
+ class AdminAbilities
142
+ include Kan::Abilities
143
+
144
+ role :admin, Container['post.roles.admin'] # or use dry-container
145
+
146
+ register(:read, :edit, :delete) { |_, _| true }
147
+ end
148
+ end
149
+ ```
150
+
151
+ ### Detect Roles
152
+
153
+ Kan allow to detect all roles for specific payload. For this use `roles` calls in scope:
154
+
155
+ ```ruby
156
+ module Post
157
+ class AnonymousAbilities
158
+ include Kan::Abilities
159
+
160
+ role :anonymous, Roles::Anonymous.new
161
+ register(:read, :edit, :delete) { false }
162
+ end
163
+
164
+ class AdminAbilities
165
+ include Kan::Abilities
166
+
167
+ role :admin, Roles::Admin.new
168
+ register(:read, :edit, :delete) { |_, _| true }
169
+ end
170
+ end
171
+
172
+ abilities = Kan::Application.new(
173
+ post: [AnonymousAbilities.new, AdminAbilities.new]
174
+ )
175
+
176
+ abilities['post.roles'].call(anonymous_user, payload) # => [:anonymous]
177
+ abilities['post.roles'].call(admin_user, payload) # => [:anonymous, :admin]
178
+ ```
@@ -0,0 +1,127 @@
1
+ ## Testing
2
+
3
+ For expample we have a simple kan class:
4
+
5
+ ```ruby
6
+
7
+ module Comments
8
+ module Roles
9
+ class Admin
10
+ def call(user, _)
11
+ user.admin?
12
+ end
13
+ end
14
+ end
15
+
16
+ class Abilities
17
+ include Kan::Abilities
18
+
19
+ role :admin, Roles::Admin.new
20
+
21
+ register('read') { |user, _| user&.id }
22
+
23
+ register('edit') do |user, comment|
24
+ user.id == comment.id || user.admin?
25
+ end
26
+ end
27
+ end
28
+ ```
29
+
30
+ ### Ability
31
+
32
+ For testing specific ability use `#ability` Abilities method:
33
+
34
+ ```ruby
35
+ RSpec.describe Comments::Abilities do
36
+ let(:abilities) { described_class.new }
37
+
38
+ subject { ability.call(account, nil) }
39
+
40
+ describe 'read ability' do
41
+ let(:ability) { abilities.ability(:read) } # it will return proc object
42
+
43
+ context 'when user login' do
44
+ let(:user) { User.new(id: 1) }
45
+
46
+ it { expect(subject).to eq true }
47
+ end
48
+
49
+ context 'when user anonymous' do
50
+ let(:user) { User.new(id: 1) }
51
+
52
+ it { expect(subject).to eq false }
53
+ end
54
+ end
55
+ end
56
+ ```
57
+
58
+ Or testing specific ability using custom matchers:
59
+
60
+ ```ruby
61
+ RSpec.describe Comments::Abilities, type: :ability do
62
+ subject do
63
+ Kan::Application.new(
64
+ user: Users::Abilities.new,
65
+ comment: Comments::Abilities.new
66
+ )
67
+ end
68
+
69
+ describe 'read ability' do
70
+ context 'when user login' do
71
+ let(:user) { User.new(id: 1) }
72
+ it { is_expected.to permit('comment.read', user) }
73
+ end
74
+
75
+ context 'when user anonymous' do
76
+ let(:user) { User.new(id: 1) }
77
+ it { is_expected.not_to permit('comment.read', user) }
78
+ end
79
+ end
80
+ end
81
+ ```
82
+
83
+ ### Role
84
+
85
+ For testing role you can use two ways. The first - test role object:
86
+
87
+ ```ruby
88
+ RSpec.describe Comments::Abilities, type: :ability do
89
+ let(:role) { Comments::Role::Admin.new }
90
+
91
+ subject { role.call(user) }
92
+
93
+ context 'when user admin' do
94
+ let(:user) { User.new(admin: true) }
95
+
96
+ it { expect(subject).to be_true }
97
+ end
98
+
99
+ context 'when user anonymous' do
100
+ let(:user) { User.new(admin: false) }
101
+
102
+ it { expect(subject).to be_false }
103
+ end
104
+ end
105
+ ```
106
+
107
+ Or use `#role_block` class method:
108
+
109
+ ```ruby
110
+ RSpec.describe Comments::Abilities, type: :ability do
111
+ let(:role) { Comments::Abilities.role_block }
112
+
113
+ subject { role.call(user) }
114
+
115
+ context 'when user admin' do
116
+ let(:user) { User.new(admin: true) }
117
+
118
+ it { expect(subject).to be_true }
119
+ end
120
+
121
+ context 'when user anonymous' do
122
+ let(:user) { User.new(admin: false) }
123
+
124
+ it { expect(subject).to be_false }
125
+ end
126
+ end
127
+ ```
@@ -0,0 +1,21 @@
1
+ ## Dry-auto\_inject
2
+
3
+ ```ruby
4
+ AbilitiesImport = Dry::AutoInject(Kan::Application.new({}))
5
+
6
+ # Operation
7
+
8
+ class UpdateOperation
9
+ include AbilitiesImport[ability_checker: 'post.edit']
10
+
11
+ def call(user, params)
12
+ return Left(:permission_denied) unless ability_checker.call(user)
13
+ # ...
14
+ end
15
+ end
16
+
17
+ # Specs
18
+
19
+ UpdateOperation.new(ability_checker: ->(*) { true })
20
+ UpdateOperation.new(ability_checker: ->(*) { false })
21
+ ```
@@ -6,18 +6,24 @@ module Kan
6
6
  base.extend(ClassMethods)
7
7
  end
8
8
 
9
+ class InvalidRoleObjectError < StandardError; end
10
+ class InvalidAbilityNameError < StandardError; end
11
+
9
12
  module ClassMethods
10
13
  DEFAULT_ROLE_NAME = :base
11
14
  DEFAULT_ROLE_BLOCK = proc { true }
12
15
 
13
16
  def register(*abilities, &block)
17
+ abilities.map!(&:to_sym)
18
+ raise InvalidAbilityNameError if abilities.include?(:roles)
19
+
14
20
  @ability_list ||= {}
15
- abilities.each { |ability| @ability_list[ability.to_sym] = block }
21
+ abilities.each { |ability| @ability_list[ability] = block }
16
22
  end
17
23
 
18
- def role(role_name, &block)
24
+ def role(role_name, object = nil, &block)
19
25
  @role_name = role_name
20
- @role_block = block
26
+ @role_block = object ? make_callable(object) : block
21
27
  end
22
28
 
23
29
  def role_name
@@ -35,6 +41,16 @@ module Kan
35
41
  def ability_list
36
42
  @ability_list || {}
37
43
  end
44
+
45
+ private
46
+
47
+ def make_callable(object)
48
+ callable_object = object.is_a?(Class) ? object.new : object
49
+
50
+ return callable_object if callable_object.respond_to? :call
51
+
52
+ raise InvalidRoleObjectError.new "role object #{object} does not support #call method"
53
+ end
38
54
  end
39
55
 
40
56
  DEFAULT_ABILITY_BLOCK = proc { true }
@@ -48,7 +64,7 @@ module Kan
48
64
 
49
65
  def ability(name)
50
66
  rule = self.class.ability_list[name.to_sym] || @options[:default_ability_block] || DEFAULT_ABILITY_BLOCK
51
- lambda { |*args| instance_exec(args, &rule) }
67
+ ->(*args) { instance_exec(args, &rule) }
52
68
  end
53
69
  end
54
70
  end
@@ -1,14 +1,28 @@
1
1
  module Kan
2
2
  class AbilitiesList
3
+ ROLES_DETECT = 'roles'.freeze
4
+
3
5
  def initialize(name, list)
4
6
  @name = name
5
7
  @list = list
6
8
  end
7
9
 
8
10
  def call(*payload)
11
+ @name == ROLES_DETECT ? mapped_roles(payload) : ability_check(payload)
12
+ end
13
+
14
+ private
15
+
16
+ def ability_check(payload)
9
17
  @list
10
18
  .select { |abilities| abilities.class.valid_role?(*payload) }
11
19
  .any? { |abilities| abilities.ability(@name).call(*payload) }
12
20
  end
21
+
22
+ def mapped_roles(payload)
23
+ @list.map do |abilities|
24
+ abilities.class.valid_role?(*payload) ? abilities.class.role_name : nil
25
+ end.compact
26
+ end
13
27
  end
14
28
  end
@@ -1,16 +1,23 @@
1
1
  module Kan
2
2
  class Application
3
- def initialize(scopes)
3
+ class InvalidScopeError < StandardError; end
4
+
5
+ def initialize(scopes = {})
6
+ raise(InvalidScopeError) unless scopes.is_a?(Hash)
7
+ raise(InvalidScopeError) if scopes.empty?
8
+
4
9
  @scopes = Hash(scopes)
10
+ @abilities_lists = {}
5
11
  end
6
12
 
7
13
  def [](ability)
8
14
  scope, ability_name = ability.split('.')
9
-
10
15
  abilities = Array(@scopes[scope.to_sym])
16
+
11
17
  raise_scope_error(scope) if abilities.empty?
18
+ return @abilities_lists[ability] if @abilities_lists[ability]
12
19
 
13
- AbilitiesList.new(ability_name, abilities)
20
+ @abilities_lists[ability] = AbilitiesList.new(ability_name, abilities)
14
21
  end
15
22
 
16
23
  private
@@ -0,0 +1,64 @@
1
+ module Kan
2
+ module RSpec
3
+ module Matchers
4
+ extend ::RSpec::Matchers::DSL
5
+
6
+ matcher :permit do |ability, *targets|
7
+ match_proc = lambda do |app|
8
+ app[ability].call(*targets)
9
+ end
10
+
11
+ match_when_negated_proc = lambda do |app|
12
+ !app[ability].call(*targets)
13
+ end
14
+
15
+ failure_message_proc = lambda do |_app|
16
+ target, action = ability.split('.')
17
+ "Expected #{target} to grant #{action} but not granted"
18
+ end
19
+
20
+ failure_message_when_negated_proc = lambda do |_app|
21
+ target, action = ability.split('.')
22
+ "Expected #{target} not to grant #{action} but granted"
23
+ end
24
+
25
+ match(&match_proc)
26
+ match_when_negated(&match_when_negated_proc)
27
+ failure_message(&failure_message_proc)
28
+ failure_message_when_negated(&failure_message_when_negated_proc)
29
+ end
30
+ end
31
+
32
+ module DSL
33
+ def permissions(&block)
34
+ describe(caller: caller) { instance_eval(&block) }
35
+ end
36
+ end
37
+
38
+ module AbilityExampleGroup
39
+ include Kan::RSpec::Matchers
40
+
41
+ def self.included(base)
42
+ base.metadata[:type] = :ability
43
+ base.extend Kan::RSpec::DSL
44
+ super
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ RSpec.configure do |config|
51
+ if RSpec::Core::Version::STRING.split(".").first.to_i >= 3
52
+ config.include(
53
+ Kan::RSpec::AbilityExampleGroup,
54
+ type: :ability,
55
+ file_path: %r{spec/abilites}
56
+ )
57
+ else
58
+ config.include(
59
+ Kan::RSpec::AbilityExampleGroup,
60
+ type: :ability,
61
+ example_group: { file_path: %r{spec/abilites} }
62
+ )
63
+ end
64
+ end
@@ -1,3 +1,3 @@
1
1
  module Kan
2
- VERSION = "0.2.0"
2
+ VERSION = "0.4.0".freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kan
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Anton Davydov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-01-21 00:00:00.000000000 Z
11
+ date: 2018-06-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -75,6 +75,7 @@ extra_rdoc_files: []
75
75
  files:
76
76
  - ".gitignore"
77
77
  - ".rspec"
78
+ - ".rubocop.yml"
78
79
  - ".travis.yml"
79
80
  - CHANGELOG.md
80
81
  - CODE_OF_CONDUCT.md
@@ -83,11 +84,19 @@ files:
83
84
  - LICENSE.txt
84
85
  - README.md
85
86
  - Rakefile
87
+ - docs/_config.yml
88
+ - docs/_layouts/default.html
89
+ - docs/faq.md
90
+ - docs/index.md
91
+ - docs/roles.md
92
+ - docs/testing.md
93
+ - docs/working_with_dry.md
86
94
  - kan.gemspec
87
95
  - lib/kan.rb
88
96
  - lib/kan/abilities.rb
89
97
  - lib/kan/abilities_list.rb
90
98
  - lib/kan/application.rb
99
+ - lib/kan/rspec.rb
91
100
  - lib/kan/version.rb
92
101
  homepage: https://github.com/davydovanton/kan
93
102
  licenses:
@@ -109,7 +118,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
109
118
  version: '0'
110
119
  requirements: []
111
120
  rubyforge_project:
112
- rubygems_version: 2.6.13
121
+ rubygems_version: 2.7.3
113
122
  signing_key:
114
123
  specification_version: 4
115
124
  summary: Simple, light and functional authorization library