permisi 0.1.1 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +8 -1
- data/CHANGELOG.md +8 -3
- data/Gemfile +2 -0
- data/Gemfile.lock +4 -1
- data/README.md +79 -10
- data/lib/generators/permisi/templates/initializer.rb +4 -0
- data/lib/permisi.rb +1 -1
- data/lib/permisi/backend/active_record/actor.rb +23 -3
- data/lib/permisi/config.rb +14 -2
- data/lib/permisi/permission_util.rb +9 -1
- data/lib/permisi/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6f5921c398a6ce7ae469c520df94b9fa41c9ee9e43008acfad87ad45c2252000
|
4
|
+
data.tar.gz: 467511ff2872e5a4b42cf01b18b4578ac3c8e45b42b4dba7ce2354ff65303d81
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7740edf97adf3444c8051b6d126f148851467aad17c80b73ab43ff505a8201ba5eedb0c681ce85cadcac354e377a92a888b4f19b51a3b72ab353bdc3ec070ced
|
7
|
+
data.tar.gz: 491438a44e151e82cadf211a2ffd8d6cffd0fab84e321553a0089c10f29f95acc5463ca2acaa9ced5f888d5c9a50cbaf8c513cab5ca24c46dc73487b6a4927b7
|
data/.github/workflows/ci.yml
CHANGED
@@ -4,10 +4,15 @@ on:
|
|
4
4
|
push:
|
5
5
|
branches:
|
6
6
|
- main
|
7
|
+
tags:
|
8
|
+
- '!*'
|
7
9
|
pull_request:
|
10
|
+
paths:
|
11
|
+
- '!*.MD'
|
12
|
+
- '!*.md'
|
8
13
|
|
9
14
|
jobs:
|
10
|
-
|
15
|
+
test:
|
11
16
|
runs-on: ubuntu-latest
|
12
17
|
|
13
18
|
steps:
|
@@ -44,3 +49,5 @@ jobs:
|
|
44
49
|
|
45
50
|
- name: Run RSpec
|
46
51
|
run: bundle exec rake spec
|
52
|
+
env:
|
53
|
+
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
# 0.1.2
|
4
|
+
|
5
|
+
- Fix namespaces/actions should no longer contain periods
|
6
|
+
- Implement cache config for faster access to actor permissions
|
7
|
+
|
3
8
|
# 0.1.1
|
4
9
|
|
5
10
|
- General code refactoring
|
@@ -11,9 +16,9 @@
|
|
11
16
|
|
12
17
|
Finished extraction work from my past projects.
|
13
18
|
|
14
|
-
-
|
15
|
-
-
|
16
|
-
-
|
19
|
+
- Implement ActiveRecord backend
|
20
|
+
- Implement `Actable` mixin
|
21
|
+
- Implement permissions hash sanitization and checking
|
17
22
|
|
18
23
|
# 0.0.1
|
19
24
|
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
permisi (0.1.
|
4
|
+
permisi (0.1.2)
|
5
5
|
activemodel (>= 3.2.0)
|
6
6
|
activerecord (>= 3.2.0)
|
7
7
|
activesupport (>= 3.2.0)
|
@@ -23,6 +23,8 @@ GEM
|
|
23
23
|
zeitwerk (~> 2.3)
|
24
24
|
ast (2.4.2)
|
25
25
|
byebug (11.1.3)
|
26
|
+
codecov (0.4.3)
|
27
|
+
simplecov (>= 0.15, < 0.22)
|
26
28
|
concurrent-ruby (1.1.8)
|
27
29
|
diff-lcs (1.4.4)
|
28
30
|
docile (1.3.5)
|
@@ -78,6 +80,7 @@ PLATFORMS
|
|
78
80
|
|
79
81
|
DEPENDENCIES
|
80
82
|
byebug (~> 11.1)
|
83
|
+
codecov (~> 0.4.3)
|
81
84
|
permisi!
|
82
85
|
rake (~> 13.0)
|
83
86
|
rspec (~> 3.0)
|
data/README.md
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
If you're viewing this at https://github.com/ukazap/permisi, you're reading the documentation for the main branch. [Go to specific version.](https://github.com/ukazap/permisi/tags)
|
2
|
+
|
1
3
|
<table>
|
2
4
|
<tr>
|
3
5
|
<th>
|
@@ -9,8 +11,15 @@
|
|
9
11
|
<h1>Permisi</h1>
|
10
12
|
<p><em>Simple and dynamic role-based access control for Rails</em></p>
|
11
13
|
<p>
|
12
|
-
<a href="https://badge.fury.io/rb/permisi"
|
13
|
-
|
14
|
+
<a href="https://badge.fury.io/rb/permisi">
|
15
|
+
<img src="https://badge.fury.io/rb/permisi.svg" alt="Gem Version">
|
16
|
+
</a>
|
17
|
+
<a href="https://codeclimate.com/github/ukazap/permisi/maintainability">
|
18
|
+
<img src="https://api.codeclimate.com/v1/badges/0b1238302f2012b20740/maintainability" />
|
19
|
+
</a>
|
20
|
+
<a href="https://codecov.io/gh/ukazap/permisi">
|
21
|
+
<img src="https://codecov.io/gh/ukazap/permisi/branch/main/graph/badge.svg?token=9YRMVFCDA8"/>
|
22
|
+
</a>
|
14
23
|
</p>
|
15
24
|
</th>
|
16
25
|
<th>
|
@@ -143,11 +152,11 @@ admin_role.allows? "books.delete" # == true
|
|
143
152
|
|
144
153
|
## Configuring actors
|
145
154
|
|
146
|
-
You can then give or take multiple roles to an actor which will allow or prevent them to perform certain actions in a flexible manner. But before you can do that, you have to wire up your user model with Permisi.
|
155
|
+
You can then give or take multiple roles to an actor which will allow or prevent them to perform certain actions in a flexible manner. But before you can do that, you have to wire up your user model with Permisi using via `Permisi::Actable` mixin.
|
147
156
|
|
148
157
|
Permisi does not hold an assumption that a specific model is present (e.g. User model). Instead, it keeps track of "actors" internally. The goal is to support multiple use cases such as actor polymorphism, user _groups_, etc.
|
149
158
|
|
150
|
-
For example, you can map your user model to Permisi's actor model
|
159
|
+
For example, you can map your user model to Permisi's actor model like so:
|
151
160
|
|
152
161
|
```ruby
|
153
162
|
# app/models/user.rb
|
@@ -157,21 +166,81 @@ class User < ApplicationRecord
|
|
157
166
|
end
|
158
167
|
```
|
159
168
|
|
160
|
-
You can then interact
|
169
|
+
You can then interact using `#permisi` method:
|
161
170
|
|
162
171
|
```ruby
|
163
172
|
user = User.find_by_email "esther@example.com"
|
164
173
|
user.permisi # => instance of Actor
|
165
174
|
|
166
|
-
|
167
|
-
|
175
|
+
admin_role = Permisi.roles.find_by_slug(:admin)
|
176
|
+
admin_role.allows? "books.delete" # == true
|
177
|
+
|
178
|
+
user.permisi.roles << admin_role
|
179
|
+
|
180
|
+
user.permisi.role? :admin # == true
|
181
|
+
user.permisi.has_role? :admin # == user.permisi.role? :admin
|
182
|
+
|
183
|
+
user.permisi.may_i? "books.delete" # == true
|
184
|
+
user.permisi.may? "books.delete" # == user.permisi.may_i? "books.delete"
|
185
|
+
|
186
|
+
user.permisi.roles.destroy(admin_role)
|
187
|
+
|
188
|
+
user.permisi.role? :admin # == false
|
189
|
+
user.permisi.may_i? "books.delete" # == false
|
190
|
+
```
|
191
|
+
|
192
|
+
## Caching
|
193
|
+
|
194
|
+
Permisi has several optimizations out of the box: actor roles eager loading, actor permissions memoization, and the optional actor permissions caching.
|
195
|
+
|
196
|
+
### Actor roles eager loading
|
197
|
+
|
198
|
+
Although checking whether an actor has a role goes against a good RBAC practice, it is still possible on Permisi. Calling `role?` multiple times will only make one call to the database:
|
199
|
+
|
200
|
+
```ruby
|
201
|
+
user = User.find_by_email "esther@example.com"
|
202
|
+
user.role? :admin # eager loads roles
|
203
|
+
user.role? :admin # uses the eager-loaded roles
|
204
|
+
user.has_role? :admin # uses the eager-loaded roles
|
205
|
+
```
|
206
|
+
|
207
|
+
### Actor permissions memoization
|
208
|
+
|
209
|
+
To check whether or not an actor is allowed to perform a specific action (`#may_i?`), Permisi will check on the actor's permissions which is constructed in the following steps:
|
210
|
+
|
211
|
+
- get all roles an actor have (this will make a database call)
|
212
|
+
- initialize an empty aggregate hash
|
213
|
+
- for each roles, merge its permissions hash to the aggregate hash
|
168
214
|
|
169
|
-
|
215
|
+
Deserializing the hashes from the database and deeply-merging them into an aggregate hash can be expensive, so it will only happen to an instance of actor only once through memoization.
|
170
216
|
|
171
|
-
|
172
|
-
|
217
|
+
### Actor permissions caching
|
218
|
+
|
219
|
+
Although memoization helps, the permission hash construction will still occur everytime a actor is initialized. To alleviate this, we can introduce a caching layer so that we can skip the hash construction for fresh actors. You must configure a cache store to use caching:
|
220
|
+
|
221
|
+
```ruby
|
222
|
+
# config/initializers/permisi.rb
|
223
|
+
|
224
|
+
Permisi.init do |config|
|
225
|
+
# You can use the default Rails cache store
|
226
|
+
config.cache_store = Rails.cache
|
227
|
+
# or use other cache stores
|
228
|
+
config.cache_store = ActiveSupport::Cache::RedisCacheStore.new(url: ENV['REDIS_URL'])
|
229
|
+
# or
|
230
|
+
config.cache_store = ActiveSupport::Cache::FileStore.new("/home/ukazap/permisi_cache/")
|
231
|
+
end
|
173
232
|
```
|
174
233
|
|
234
|
+
You can also roll your own [custom cache store](https://guides.rubyonrails.org/caching_with_rails.html#custom-cache-stores).
|
235
|
+
|
236
|
+
### Cache/memo invalidation
|
237
|
+
|
238
|
+
The following will trigger actor's permissions cache/memo invalidation:
|
239
|
+
|
240
|
+
- adding roles to the actor
|
241
|
+
- removing roles from the actor
|
242
|
+
- editing roles that belongs to an actor
|
243
|
+
|
175
244
|
## Contributing
|
176
245
|
|
177
246
|
For development and how to submit improvements, please refer to the [contribution guide](https://github.com/ukazap/permisi/blob/main/CONTRIBUTING.md).
|
@@ -10,4 +10,8 @@ Permisi.init do |config|
|
|
10
10
|
# Define all permissions available in the system
|
11
11
|
# See https://github.com/ukazap/permisi#configuring-permissions
|
12
12
|
config.permissions = {}
|
13
|
+
|
14
|
+
# Define cache store
|
15
|
+
# See https://github.com/ukazap/permisi#caching
|
16
|
+
config.cache_store = Rails.cache
|
13
17
|
end
|
data/lib/permisi.rb
CHANGED
@@ -8,15 +8,35 @@ module Permisi
|
|
8
8
|
has_many :actor_roles, dependent: :destroy
|
9
9
|
has_many :roles, through: :actor_roles
|
10
10
|
|
11
|
+
after_commit :reset_permissions
|
12
|
+
|
11
13
|
def role?(role_slug)
|
12
14
|
roles.load.any? { |role| role.slug == role_slug.to_s }
|
13
15
|
end
|
14
16
|
|
15
|
-
def
|
16
|
-
|
17
|
+
def may_i?(action_path)
|
18
|
+
PermissionUtil.allows?(permissions, action_path)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Memoized and cached actor permissions
|
22
|
+
def permissions
|
23
|
+
@permissions ||= Permisi.config.cache_store.fetch(cache_key) { aggregate_permissions }
|
24
|
+
end
|
25
|
+
|
26
|
+
# Aggregate permissions from all roles an actor plays
|
27
|
+
def aggregate_permissions
|
28
|
+
roles.load.inject(HashWithIndifferentAccess.new) do |aggregate, role|
|
29
|
+
aggregate.deep_merge(role.permissions) do |_key, effect, another_effect|
|
30
|
+
effect == true || another_effect == true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def reset_permissions
|
36
|
+
@permissions = nil
|
17
37
|
end
|
18
38
|
|
19
|
-
alias
|
39
|
+
alias may? may_i?
|
20
40
|
alias has_role? role?
|
21
41
|
end
|
22
42
|
end
|
data/lib/permisi/config.rb
CHANGED
@@ -1,9 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "active_support/core_ext/class/attribute_accessors"
|
4
|
-
|
5
3
|
module Permisi
|
6
4
|
class Config
|
5
|
+
class InvalidCacheStore < StandardError; end
|
6
|
+
|
7
|
+
NULL_CACHE_STORE = ActiveSupport::Cache::NullStore.new
|
8
|
+
|
7
9
|
attr_reader :permissions, :default_permissions
|
8
10
|
|
9
11
|
def initialize
|
@@ -27,5 +29,15 @@ module Permisi
|
|
27
29
|
@default_permissions = PermissionUtil.transform_namespace(permissions_hash)
|
28
30
|
@permissions = permissions_hash
|
29
31
|
end
|
32
|
+
|
33
|
+
def cache_store=(cache_store)
|
34
|
+
raise InvalidCacheStore unless cache_store.respond_to?(:fetch)
|
35
|
+
|
36
|
+
@cache_store = cache_store
|
37
|
+
end
|
38
|
+
|
39
|
+
def cache_store
|
40
|
+
@cache_store || NULL_CACHE_STORE
|
41
|
+
end
|
30
42
|
end
|
31
43
|
end
|
@@ -20,14 +20,22 @@ module Permisi
|
|
20
20
|
def transform_namespace(namespace, current_path: nil)
|
21
21
|
HashWithIndifferentAccess.new.tap do |transformed|
|
22
22
|
namespace.each_pair do |key, value|
|
23
|
-
|
23
|
+
if !value.is_a? Array
|
24
24
|
raise InvalidNamespace,
|
25
25
|
"`#{[current_path, key].compact.join(".")}` should be an array"
|
26
26
|
end
|
27
27
|
|
28
|
+
if key.to_s.include?(".")
|
29
|
+
raise InvalidNamespace, "namespace or action should not contain period: `#{key}`"
|
30
|
+
end
|
31
|
+
|
28
32
|
value.each.with_index do |arr_v, arr_i|
|
29
33
|
case arr_v
|
30
34
|
when Symbol
|
35
|
+
if arr_v.to_s.include?(".")
|
36
|
+
raise InvalidNamespace, "namespace or action should not contain period: `#{arr_v}`"
|
37
|
+
end
|
38
|
+
|
31
39
|
transformed[key] ||= ::HashWithIndifferentAccess.new
|
32
40
|
if transformed[key].key? arr_v
|
33
41
|
raise InvalidNamespace, "duplicate entry: `#{[current_path, key, arr_v].compact.join(".")}`"
|
data/lib/permisi/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: permisi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ukaza Perdana
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-02-
|
11
|
+
date: 2021-02-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activemodel
|
@@ -133,7 +133,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
133
133
|
- !ruby/object:Gem::Version
|
134
134
|
version: '0'
|
135
135
|
requirements: []
|
136
|
-
rubygems_version: 3.
|
136
|
+
rubygems_version: 3.2.3
|
137
137
|
signing_key:
|
138
138
|
specification_version: 4
|
139
139
|
summary: Simple and dynamic role-based access control for Rails
|