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 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