kan 0.2.0 → 0.4.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.
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