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 +5 -5
- data/.gitignore +1 -0
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +12 -0
- data/Gemfile +3 -1
- data/Gemfile.lock +18 -1
- data/README.md +22 -126
- data/Rakefile +1 -1
- data/docs/_config.yml +3 -0
- data/docs/_layouts/default.html +38 -0
- data/docs/faq.md +24 -0
- data/docs/index.md +81 -0
- data/docs/roles.md +178 -0
- data/docs/testing.md +127 -0
- data/docs/working_with_dry.md +21 -0
- data/lib/kan/abilities.rb +20 -4
- data/lib/kan/abilities_list.rb +14 -0
- data/lib/kan/application.rb +10 -3
- data/lib/kan/rspec.rb +64 -0
- data/lib/kan/version.rb +1 -1
- metadata +12 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7905462f6cf7046f2348d0fd4a72eeaa8388d65ca37e82d3b1817adcb8b0b5c5
|
4
|
+
data.tar.gz: 1e55a1e1b765f290110270a3ecf49fc9335979701030b3273f2ac155fb6918cc
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e431fe2a8833613fcfdcf5735c33a42e8627fa7ec57bb22a1560a5a4cfeae5b048c44b3adec766fff2c8db09ebf599654d2d84179301479b3ae8d7a11dc41d9
|
7
|
+
data.tar.gz: e2e3396a2f552d3c326a09cd707c1fa9c40cd92ce7e171e718a1f61f7f810fe804c9f25061673d2d0de8f5b32003e2920e58769972019ba5e287e2adba18df96
|
data/.gitignore
CHANGED
data/.rubocop.yml
ADDED
@@ -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/**/*"
|
data/CHANGELOG.md
CHANGED
@@ -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
|
data/Gemfile.lock
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
kan (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
|
45
|
-
register
|
46
|
-
register
|
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
|
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
|
63
|
-
register
|
66
|
+
register('read') { |_, _| true }
|
67
|
+
register('edit') { |user, _| user.admin? }
|
64
68
|
|
65
|
-
register
|
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
data/docs/_config.yml
ADDED
@@ -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>
|
data/docs/faq.md
ADDED
@@ -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
|
+
```
|
data/docs/index.md
ADDED
@@ -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
|
+
```
|
data/docs/roles.md
ADDED
@@ -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
|
+
```
|
data/docs/testing.md
ADDED
@@ -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
|
+
```
|
data/lib/kan/abilities.rb
CHANGED
@@ -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
|
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
|
-
|
67
|
+
->(*args) { instance_exec(args, &rule) }
|
52
68
|
end
|
53
69
|
end
|
54
70
|
end
|
data/lib/kan/abilities_list.rb
CHANGED
@@ -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
|
data/lib/kan/application.rb
CHANGED
@@ -1,16 +1,23 @@
|
|
1
1
|
module Kan
|
2
2
|
class Application
|
3
|
-
|
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
|
data/lib/kan/rspec.rb
ADDED
@@ -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
|
data/lib/kan/version.rb
CHANGED
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.
|
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-
|
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.
|
121
|
+
rubygems_version: 2.7.3
|
113
122
|
signing_key:
|
114
123
|
specification_version: 4
|
115
124
|
summary: Simple, light and functional authorization library
|