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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50d92f00f694bb0b93a36125736bbd3afae15446c6fe5c71e0a83027a136c5e8
4
- data.tar.gz: f214d63982175a708e99f7c9238a778b10ec4b599b8baed0dbf09e62a3ad0ae4
3
+ metadata.gz: 6f5921c398a6ce7ae469c520df94b9fa41c9ee9e43008acfad87ad45c2252000
4
+ data.tar.gz: 467511ff2872e5a4b42cf01b18b4578ac3c8e45b42b4dba7ce2354ff65303d81
5
5
  SHA512:
6
- metadata.gz: ea1cedd52afd42383027297028974947aa338eff64d0bb12daee7f3e73663dc054c983f792f6ec8a917f2273745cbb212ea17d12271d11de003a509f53111803
7
- data.tar.gz: 395586b6b62fdedfe3c9ffc0ea824868cf27d76922af4a7501f161eabd06561c9d44d251a114685552e4d5a37c50923b721621d212ef3d956f98a025e93f4649
6
+ metadata.gz: 7740edf97adf3444c8051b6d126f148851467aad17c80b73ab43ff505a8201ba5eedb0c681ce85cadcac354e377a92a888b4f19b51a3b72ab353bdc3ec070ced
7
+ data.tar.gz: 491438a44e151e82cadf211a2ffd8d6cffd0fab84e321553a0089c10f29f95acc5463ca2acaa9ced5f888d5c9a50cbaf8c513cab5ca24c46dc73487b6a4927b7
@@ -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
- build:
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
- - Implemented ActiveRecord backend
15
- - Implemented `Actable` mixin
16
- - Implemented permissions hash sanitization and checking
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
@@ -11,3 +11,5 @@ gem "rspec", "~> 3.0"
11
11
  gem "rubocop", "~> 1.9"
12
12
  gem "simplecov", "~> 0.21.2"
13
13
  gem "sqlite3", "~> 1.4"
14
+
15
+ gem "codecov", "~> 0.4.3"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- permisi (0.1.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"><img src="https://badge.fury.io/rb/permisi.svg" alt="Gem Version"></a>
13
- <a href="https://codeclimate.com/github/ukazap/permisi/maintainability"><img src="https://api.codeclimate.com/v1/badges/0b1238302f2012b20740/maintainability" /></a>
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 by including the `Permisi::Actable` mixin like so:
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 with the new `#permisi` method:
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
- user.permisi.has_role? :admin # == false
167
- user.permisi.may? "books.delete" # == false
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
- user.permisi.roles << Permisi.roles.find_by_slug(:admin)
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
- user.permisi.has_role? :admin # == true
172
- user.permisi.may? "books.delete" # == true
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/type"
4
- require "active_support/hash_with_indifferent_access"
4
+ require "active_support"
5
5
  require "zeitwerk"
6
6
 
7
7
  module Permisi
@@ -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 may?(action_path)
16
- roles.load.any? { |role| role.allows?(action_path) }
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 may_i? may?
39
+ alias may? may_i?
20
40
  alias has_role? role?
21
41
  end
22
42
  end
@@ -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
- unless value.is_a? Array
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(".")}`"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Permisi
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
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.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-20 00:00:00.000000000 Z
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.1.4
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