permisi 0.1.1 → 0.1.2
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 +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
|