permisi 0.1.0 → 0.1.5

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: 47559ea904f406010a7979cd77de853bf8c3628c7e4c1020e45742ae439a7e06
4
- data.tar.gz: b8c814331a90cbc7e4d2d3560cfb939419b001f1e88e731a3e67e9b180ead167
3
+ metadata.gz: 20d261fa05615f1a2b28f377d652b0f74af4df86de9e7fbabd820259fcdb619f
4
+ data.tar.gz: e1b86647d121b1e8f975be3f29165bbc8527285a742edd7453d3e26aa3b476d5
5
5
  SHA512:
6
- metadata.gz: bdfbfbadcc257c15ded64521607e062d2d51ba3c9fc5209f4af273f69473df981e2bbbc74457e0bbd3cc0576e9c51e67895f66685bba7dada2e88d3b4513199c
7
- data.tar.gz: a095961d001eb1495f7d084434f8bef182add89e86f9a147013a34f0b7c171cff4da65373798442e45e962264f03c6d0c07eb23f58ca520e742c61adbd94d8e6
6
+ metadata.gz: 202a1ce6aced43df306e00eb02ea82058b57b22459ffbe76749836bf37d2649417ccc1fcf5e868720638789a09c00a390e0968488e3d9003efb2432f3e298435
7
+ data.tar.gz: 5cf4ed22ff8b0531542d17e37e11bb0dd4d8ce9fc8f4cc744625fa815b77285065f952abcd53c1af8dcc81e0ba59410f163068e5a5e427dd06e26393d2a40862
@@ -0,0 +1,53 @@
1
+ name: Lint and test
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ tags:
8
+ - '!*'
9
+ pull_request:
10
+ paths:
11
+ - '!*.MD'
12
+ - '!*.md'
13
+
14
+ jobs:
15
+ test:
16
+ runs-on: ubuntu-latest
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+
21
+ - name: Set up Ruby 2.7
22
+ uses: ruby/setup-ruby@v1
23
+ with:
24
+ ruby-version: 2.7
25
+
26
+ - name: Generate lockfile for cache key
27
+ run: bundle lock
28
+
29
+ - name: Cache gems
30
+ uses: actions/cache@v1
31
+ with:
32
+ path: vendor/bundle
33
+ key: ${{ runner.os }}-rspec-${{ hashFiles('**/Gemfile.lock') }}
34
+ restore-keys: |
35
+ ${{ runner.os }}-rspec-
36
+
37
+ - name: Install gems
38
+ run: |
39
+ bundle config path vendor/bundle
40
+ bundle install --jobs 4 --retry 3
41
+
42
+ - name: Run RuboCop
43
+ uses: reviewdog/action-rubocop@v1
44
+ with:
45
+ rubocop_version: gemfile
46
+ rubocop_extensions: rubocop-rails:gemfile rubocop-rspec:gemfile
47
+ github_token: ${{ secrets.github_token }}
48
+ reporter: github-pr-check # Default is github-pr-check
49
+
50
+ - name: Run RSpec
51
+ run: bundle exec rake spec
52
+ env:
53
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
data/.rubocop.yml CHANGED
@@ -1,5 +1,10 @@
1
1
  AllCops:
2
2
  TargetRubyVersion: 2.4
3
+ NewCops: enable
4
+ Exclude:
5
+ - "lib/generators/**/*"
6
+ - "spec/**/*" # stuff for later
7
+ - "lib/permisi/permission_util.rb" # The necessary evil (for now)
3
8
 
4
9
  Style/StringLiterals:
5
10
  Enabled: true
@@ -11,3 +16,7 @@ Style/StringLiteralsInInterpolation:
11
16
 
12
17
  Layout/LineLength:
13
18
  Max: 120
19
+
20
+ # Stuff for later
21
+ Documentation:
22
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -1,13 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ # 0.1.5
4
+
5
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.5/README.md)
6
+
7
+ - Remove `-> { distinct }` from actor-roles has_many association
8
+ - Add option to mute pre-0.1.4 ActiveRecord backend initialization warning:
9
+
10
+ ```ruby
11
+ # config/initializers/permisi.rb
12
+
13
+ Permisi.init do |config|
14
+ # Mute pre-0.1.4 ActiveRecord backend initialization warning
15
+ config.mute_pre_0_1_4_warning = true
16
+ end
17
+ ```
18
+
19
+ # 0.1.4
20
+
21
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.4/README.md)
22
+
23
+ - Add actor-role uniqueness constraint (previously it was possible to append the same role to an actor many times), if you are upgrading from previous versions, please create the following migration: `add_index :permisi_actor_roles, [:actor_id, :role_id], unique: true`
24
+ - Show warning when calling "roles.delete" because it won't invalidate the cache
25
+
26
+ # 0.1.3
27
+
28
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.3/README.md)
29
+
30
+ - Correct grammars and examples in the docs
31
+ - Change actor permissions cache key
32
+
33
+ # 0.1.2
34
+
35
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.2/README.md)
36
+
37
+ - Fix namespaces/actions should no longer contain periods
38
+ - Implement cache config for faster access to actor permissions
39
+
40
+ # 0.1.1
41
+
42
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.1/README.md)
43
+
44
+ - General code refactoring
45
+ - Improvements on ActiveRecord backend:
46
+ - Code refactoring
47
+ - Implement cache invalidation
48
+
3
49
  # 0.1.0
4
50
 
51
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.0/README.md)
52
+
5
53
  Finished extraction work from my past projects.
6
54
 
7
- - Implemented ActiveRecord backend
8
- - Implemented `Actable` mixin
9
- - Implemented permissions hash sanitization and checking
55
+ - Implement ActiveRecord backend
56
+ - Implement `Actable` mixin
57
+ - Implement permissions hash sanitization and checking
10
58
 
11
59
  # 0.0.1
12
60
 
61
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.0.1/README.md)
62
+
13
63
  Reserved the gem name: https://en.wiktionary.org/wiki/permisi
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,25 @@
1
+ # How to contribute
2
+
3
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ukazap/permisi/blob/master/CODE_OF_CONDUCT.md).
4
+
5
+ ## Found a bug?
6
+
7
+ - Search the [issues labeled "bug"](https://github.com/ukazap/permisi/issues?q=is%3Aissue+label%3Abug) to see if it's already reported.
8
+ - Make sure you are using the latest version of Permisi [![Gem Version](https://badge.fury.io/rb/permisi.svg)](https://badge.fury.io/rb/permisi)
9
+ - If you are still having an issue, create an issue including:
10
+ - Ruby version
11
+ - Gemfile.lock contents or at least major gem versions, such as Rails version
12
+ - Steps to reproduce the issue
13
+ - Full backtrace for any errors encountered
14
+
15
+ ## Submitting changes
16
+
17
+ If you want to contribute an enhancement or a fix:
18
+
19
+ - Fork the project on GitHub
20
+ - After checking out the repo, run `bin/setup` to install dependencies
21
+ - Make your changes with tests
22
+ - Run `bundle exec rubocop -A` to auto-format your code
23
+ - Run `rake spec` to run the tests
24
+ - Commit the changes without making changes to the Rakefile or any other files that aren't related to your enhancement or fix
25
+ - Send a pull request
data/Gemfile CHANGED
@@ -6,6 +6,7 @@ source "https://rubygems.org"
6
6
  gemspec
7
7
 
8
8
  gem "byebug", "~> 11.1"
9
+ gem "codecov", "~> 0.4.3"
9
10
  gem "rake", "~> 13.0"
10
11
  gem "rspec", "~> 3.0"
11
12
  gem "rubocop", "~> 1.9"
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- permisi (0.1.0)
4
+ permisi (0.1.5)
5
5
  activemodel (>= 3.2.0)
6
6
  activerecord (>= 3.2.0)
7
7
  activesupport (>= 3.2.0)
@@ -10,12 +10,12 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
- activemodel (6.1.2.1)
14
- activesupport (= 6.1.2.1)
15
- activerecord (6.1.2.1)
16
- activemodel (= 6.1.2.1)
17
- activesupport (= 6.1.2.1)
18
- activesupport (6.1.2.1)
13
+ activemodel (6.1.3)
14
+ activesupport (= 6.1.3)
15
+ activerecord (6.1.3)
16
+ activemodel (= 6.1.3)
17
+ activesupport (= 6.1.3)
18
+ activesupport (6.1.3)
19
19
  concurrent-ruby (~> 1.0, >= 1.0.2)
20
20
  i18n (>= 1.6, < 2)
21
21
  minitest (>= 5.1)
@@ -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)
@@ -49,7 +51,7 @@ GEM
49
51
  diff-lcs (>= 1.2.0, < 2.0)
50
52
  rspec-support (~> 3.10.0)
51
53
  rspec-support (3.10.2)
52
- rubocop (1.9.1)
54
+ rubocop (1.10.0)
53
55
  parallel (~> 1.10)
54
56
  parser (>= 3.0.0.0)
55
57
  rainbow (>= 2.2.2, < 4.0)
@@ -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/blob/main/CHANGELOG.md)
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>
@@ -133,21 +142,21 @@ admin_role = Permisi.roles.create(slug: :admin, name: "Administrator", permissio
133
142
  })
134
143
 
135
144
  # Ask specific role permission
136
- admin_role.allows? "books.delete" # == false
145
+ admin_role.allows?("books.delete") # == false
137
146
 
138
147
  # Update existing role
139
- admin.permissions[:books].merge!({ delete: true })
140
- admin.save
141
- admin_role.allows? "books.delete" # == true
148
+ admin_role.permissions[:books].merge!({ delete: true })
149
+ admin_role.save
150
+ admin_role.allows?("books.delete") # == true
142
151
  ```
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,30 +166,84 @@ 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
- user = User.find_by_email "esther@example.com"
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"
168
185
 
169
- user.permisi.roles << Permisi.roles.find_by_slug(:admin)
186
+ user.permisi.roles.destroy(admin_role)
170
187
 
171
- user.permisi.has_role? :admin # == true
172
- user.permisi.may? "books.delete" # == true
188
+ user.permisi.role?(:admin) # == false
189
+ user.permisi.may_i?("books.delete") # == false
173
190
  ```
174
191
 
175
- ## Development
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.permisi.role?(:admin) # eager loads roles
203
+ user.permisi.role?(:admin) # uses the eager-loaded roles
204
+ user.permisi.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
+ - load all the roles an actor have from the database
212
+ - initialize an empty aggregate hash
213
+ - for each role, merge its permissions hash to the aggregate hash
214
+
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.
216
+
217
+ ### Actor permissions caching
218
+
219
+ Although memoization helps, the permission hash construction will still occur every time an 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
232
+ ```
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
176
237
 
177
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
238
+ The following will trigger actor's permissions cache/memo invalidation:
178
239
 
179
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
240
+ - adding roles to the actor
241
+ - removing roles from the actor
242
+ - editing roles that belongs to the actor
180
243
 
181
244
  ## Contributing
182
245
 
183
- Bug reports and pull requests are welcome on GitHub at https://github.com/ukazap/permisi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/ukazap/permisi/blob/master/CODE_OF_CONDUCT.md).
246
+ For development and how to submit improvements, please refer to the [contribution guide](https://github.com/ukazap/permisi/blob/main/CONTRIBUTING.md).
184
247
 
185
248
  ## License
186
249
 
@@ -188,4 +251,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
188
251
 
189
252
  ## Code of Conduct
190
253
 
191
- Everyone interacting in the Permisi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ukazap/permisi/blob/master/CODE_OF_CONDUCT.md).
254
+ Everyone interacting in the Permisi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ukazap/permisi/blob/main/CODE_OF_CONDUCT.md).
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "rails/generators"
2
4
  require "rails/generators/migration"
3
5
  require "rails/generators/active_record"
@@ -7,26 +9,24 @@ module Permisi
7
9
  class InstallGenerator < Rails::Generators::Base
8
10
  include Rails::Generators::Migration
9
11
 
10
- source_root File.expand_path('../templates', __FILE__)
12
+ source_root File.expand_path("templates", __dir__)
11
13
 
12
14
  def self.next_migration_number(path)
13
15
  ActiveRecord::Generators::Base.next_migration_number(path)
14
16
  end
15
17
 
16
18
  def create_initializer
17
- template 'initializer.rb', 'config/initializers/permisi.rb'
19
+ template "initializer.rb", "config/initializers/permisi.rb"
18
20
  end
19
21
 
20
22
  def create_migrations
21
- migration_template 'migration.rb', 'db/migrate/create_permisi_tables.rb', migration_version: migration_version
23
+ migration_template "migration.rb", "db/migrate/create_permisi_tables.rb", migration_version: migration_version
22
24
  end
23
25
 
24
26
  private
25
27
 
26
28
  def migration_version
27
- if ActiveRecord.version.version > '5'
28
- "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
29
- end
29
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord.version.version > "5"
30
30
  end
31
31
  end
32
32
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "permisi"
2
4
 
3
5
  Permisi.init do |config|
@@ -8,4 +10,11 @@ Permisi.init do |config|
8
10
  # Define all permissions available in the system
9
11
  # See https://github.com/ukazap/permisi#configuring-permissions
10
12
  config.permissions = {}
13
+
14
+ # Define cache store
15
+ # See https://github.com/ukazap/permisi#caching
16
+ config.cache_store = Rails.cache
17
+
18
+ # Mute pre-0.1.4 ActiveRecord backend initialization warning
19
+ config.mute_pre_0_1_4_warning = true
11
20
  end
@@ -18,6 +18,8 @@ class CreatePermisiTables < ActiveRecord::Migration<%= migration_version %>
18
18
  t.belongs_to :actor
19
19
  t.belongs_to :role
20
20
  end
21
+
22
+ add_index :permisi_actor_roles, [:actor_id, :role_id], unique: true
21
23
  end
22
24
 
23
25
  def down
data/lib/permisi.rb CHANGED
@@ -1,41 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model/type"
4
+ require "active_support"
3
5
  require "zeitwerk"
4
- $permisi_loader = Zeitwerk::Loader.for_gem
5
- $permisi_loader.ignore("#{__dir__}/generators")
6
- $permisi_loader.ignore("#{__dir__}/permisi/backend/mongoid.rb") # todo
7
- $permisi_loader.setup
8
6
 
9
7
  module Permisi
8
+ LOADER = Zeitwerk::Loader.for_gem
9
+
10
10
  class << self
11
- def init(&block)
11
+ def init
12
12
  yield config if block_given?
13
13
  end
14
14
 
15
15
  def config
16
- @@config ||= Config.new
16
+ @config ||= Config.new
17
17
  end
18
18
 
19
19
  def actors
20
- __backend.actors
20
+ config.backend.actors
21
21
  end
22
22
 
23
23
  def actor(aka)
24
- __backend.findsert_actor(aka)
24
+ config.backend.findsert_actor(aka)
25
25
  end
26
26
 
27
27
  def roles
28
- __backend.roles
29
- end
30
-
31
- private
32
-
33
- def __backend
34
- if config.backend.nil? || !(config.backend <= Backend::Base)
35
- raise Backend::InvalidBackend
36
- end
37
-
38
- config.backend
28
+ config.backend.roles
39
29
  end
40
30
  end
41
31
  end
32
+
33
+ Permisi::LOADER.ignore("#{__dir__}/generators")
34
+ Permisi::LOADER.ignore("#{__dir__}/permisi/backend/mongoid.rb") # todo
35
+ Permisi::LOADER.setup
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Permisi
2
4
  module Actable
3
5
  def permisi_actor
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
4
+ module Backend
5
+ class InvalidBackend < StandardError
6
+ def initialize(message = "Please check https://github.com/ukazap/permisi#configuring-backend")
7
+ super
8
+ end
9
+ end
10
+
11
+ module NullBackend
12
+ class << self
13
+ def findsert_actor(_aka)
14
+ raise InvalidBackend
15
+ end
16
+
17
+ def actors
18
+ raise InvalidBackend
19
+ end
20
+
21
+ def roles
22
+ raise InvalidBackend
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -1,22 +1,26 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "active_record"
2
4
 
3
5
  module Permisi
4
- class Backend::ActiveRecord < Backend::Base
5
- class << self
6
- def table_name_prefix
7
- "permisi_"
8
- end
6
+ module Backend
7
+ module ActiveRecord
8
+ class << self
9
+ def table_name_prefix
10
+ "permisi_"
11
+ end
9
12
 
10
- def findsert_actor(aka)
11
- Actor.find_or_create_by(aka: aka)
12
- end
13
+ def findsert_actor(aka)
14
+ Actor.find_or_create_by(aka: aka)
15
+ end
13
16
 
14
- def actors
15
- Actor.all
16
- end
17
+ def actors
18
+ Actor.all
19
+ end
17
20
 
18
- def roles
19
- Role.all
21
+ def roles
22
+ Role.all
23
+ end
20
24
  end
21
25
  end
22
26
  end
@@ -1,15 +1,61 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Permisi
2
- class Backend::ActiveRecord::Actor < ::ActiveRecord::Base
3
- belongs_to :aka, polymorphic: true
4
- has_many :actor_roles
5
- has_many :roles, through: :actor_roles
4
+ module Backend
5
+ module ActiveRecord
6
+ class Actor < ::ActiveRecord::Base
7
+ belongs_to :aka, polymorphic: true, touch: true
8
+ has_many :actor_roles, dependent: :destroy
9
+ has_many :roles, through: :actor_roles
6
10
 
7
- def has_role?(role_slug)
8
- roles.load.any? { |role| role.slug == role_slug.to_s }
9
- end
11
+ after_commit :reset_permissions
12
+
13
+ def roles
14
+ super.extend(ActorRolesCollectionProxy)
15
+ end
16
+
17
+ def role?(role_slug)
18
+ roles.load.any? { |role| role.slug == role_slug.to_s }
19
+ end
20
+
21
+ def may_i?(action_path)
22
+ PermissionUtil.allows?(permissions, action_path)
23
+ end
24
+
25
+ # Memoized and cached actor permissions
26
+ def permissions
27
+ @permissions ||= Permisi.config.cache_store.fetch("#{cache_key}-p") { aggregate_permissions }
28
+ end
29
+
30
+ # Aggregate permissions from all roles an actor plays
31
+ def aggregate_permissions
32
+ roles.load.inject(HashWithIndifferentAccess.new) do |aggregate, role|
33
+ aggregate.deep_merge(role.permissions) do |_key, effect, another_effect|
34
+ effect == true || another_effect == true
35
+ end
36
+ end
37
+ end
38
+
39
+ def reset_permissions
40
+ @permissions = nil
41
+ end
42
+
43
+ alias may? may_i?
44
+ alias has_role? role?
45
+
46
+ module ActorRolesCollectionProxy
47
+ def <<(new_role)
48
+ super
49
+ rescue ::ActiveRecord::RecordNotUnique
50
+ self
51
+ end
10
52
 
11
- def may?(action_path)
12
- roles.load.any? { |role| role.allows?(action_path) }
53
+ def delete(*records)
54
+ warn "WARNING: `#delete(*records)` won't invalidate the cache, use `#destroy(*records)` instead."
55
+ super
56
+ end
57
+ end
58
+ end
13
59
  end
14
60
  end
15
61
  end
@@ -1,6 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Permisi
2
- class Backend::ActiveRecord::ActorRole < ::ActiveRecord::Base
3
- belongs_to :actor
4
- belongs_to :role
4
+ module Backend
5
+ module ActiveRecord
6
+ class ActorRole < ::ActiveRecord::Base
7
+ belongs_to :actor, touch: true
8
+ belongs_to :role
9
+
10
+ after_destroy :touch_actor
11
+
12
+ private
13
+
14
+ def touch_actor
15
+ actor.touch
16
+ end
17
+ end
18
+ end
5
19
  end
6
20
  end
@@ -1,27 +1,41 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Permisi
2
- class Backend::ActiveRecord::Role < ::ActiveRecord::Base
3
- has_many :actor_roles
4
- has_many :actors, through: :actor_roles
5
- has_many :akas, through: :actors
4
+ module Backend
5
+ module ActiveRecord
6
+ class Role < ::ActiveRecord::Base
7
+ has_many :actor_roles, dependent: :destroy
8
+ has_many :actors, through: :actor_roles
9
+ has_many :akas, through: :actors
6
10
 
7
- validates_presence_of :name, :slug
8
- validates_uniqueness_of :name, :slug
11
+ validates_presence_of :name, :slug
12
+ validates_uniqueness_of :name, :slug
9
13
 
10
- after_initialize :set_default_permissions
11
- before_validation :sanitize_permissions
14
+ after_initialize :set_default_permissions
15
+ before_validation :sanitize_attributes
16
+ after_update :touch_actor_roles
12
17
 
13
- serialize :permissions, Permisi::PermissionUtil::Serializer
18
+ serialize :permissions, Permisi::PermissionUtil::Serializer
14
19
 
15
- def allows?(action_path)
16
- Permisi::PermissionUtil.allows?(self.permissions, action_path)
17
- end
20
+ def allows?(action_path)
21
+ Permisi::PermissionUtil.allows?(permissions, action_path)
22
+ end
18
23
 
19
- def set_default_permissions
20
- self.permissions ||= Permisi.config.default_permissions if self.new_record?
21
- end
24
+ private
25
+
26
+ def set_default_permissions
27
+ self.permissions ||= HashWithIndifferentAccess.new if new_record?
28
+ end
29
+
30
+ def sanitize_attributes
31
+ self.name ||= slug.try(:titleize)
32
+ self.permissions = Permisi::PermissionUtil.sanitize_permissions(self.permissions)
33
+ end
22
34
 
23
- def sanitize_permissions
24
- self.permissions = Permisi::PermissionUtil.sanitize_permissions(self.permissions)
35
+ def touch_actor_roles
36
+ actor_roles.each(&:touch)
37
+ end
38
+ end
25
39
  end
26
40
  end
27
41
  end
@@ -1,5 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Permisi
2
- class Backend::Mongoid < Backend::Base
3
- raise "under construction"
4
+ module Backend
5
+ module Mongoid
6
+ raise "under construction"
7
+ end
4
8
  end
5
9
  end
@@ -1,9 +1,13 @@
1
- require "active_support/core_ext/class/attribute_accessors"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Permisi
4
4
  class Config
5
- attr_accessor :backend, :permissions
6
- attr_reader :default_permissions
5
+ class InvalidCacheStore < StandardError; end
6
+
7
+ NULL_CACHE_STORE = ActiveSupport::Cache::NullStore.new
8
+
9
+ attr_reader :permissions, :default_permissions
10
+ attr_accessor :mute_pre_0_1_4_warning
7
11
 
8
12
  def initialize
9
13
  @permissions = ::HashWithIndifferentAccess.new
@@ -11,8 +15,15 @@ module Permisi
11
15
  end
12
16
 
13
17
  def backend=(chosen_backend)
14
- if chosen_backend.is_a? Symbol
15
- chosen_backend = "::Permisi::Backend::#{chosen_backend.to_s.classify}".constantize
18
+ chosen_backend = "::Permisi::Backend::#{chosen_backend.to_s.classify}".constantize if chosen_backend.is_a? Symbol
19
+
20
+ if !mute_pre_0_1_4_warning && chosen_backend == Backend::ActiveRecord
21
+ warn <<~MESSAGE
22
+
23
+ WARNING: If you are upgrading from Permisi <v0.1.4, please create the following migration:
24
+ `add_index :permisi_actor_roles, [:actor_id, :role_id], unique: true`
25
+
26
+ MESSAGE
16
27
  end
17
28
 
18
29
  @backend = chosen_backend
@@ -20,10 +31,24 @@ module Permisi
20
31
  raise Backend::InvalidBackend
21
32
  end
22
33
 
34
+ def backend
35
+ @backend || Backend::NullBackend
36
+ end
37
+
23
38
  def permissions=(permissions_hash)
24
39
  permissions_hash = HashWithIndifferentAccess.new(permissions_hash)
25
40
  @default_permissions = PermissionUtil.transform_namespace(permissions_hash)
26
41
  @permissions = permissions_hash
27
42
  end
43
+
44
+ def cache_store=(cache_store)
45
+ raise InvalidCacheStore unless cache_store.respond_to?(:fetch)
46
+
47
+ @cache_store = cache_store
48
+ end
49
+
50
+ def cache_store
51
+ @cache_store || NULL_CACHE_STORE
52
+ end
28
53
  end
29
54
  end
@@ -1,5 +1,4 @@
1
- require "active_model/type"
2
- require "active_support/hash_with_indifferent_access"
1
+ # frozen_string_literal: true
3
2
 
4
3
  module Permisi
5
4
  module PermissionUtil
@@ -7,32 +6,47 @@ module Permisi
7
6
 
8
7
  class << self
9
8
  def allows?(hash, action_path)
10
- return false unless hash.kind_of?(Hash)
9
+ return false unless hash.is_a?(Hash)
11
10
 
12
11
  action_path_arr = action_path.split(".")
13
- (!Permisi.config.default_permissions.dig(*action_path_arr).nil? rescue false) &&
12
+ begin
13
+ !Permisi.config.default_permissions.dig(*action_path_arr).nil?
14
+ rescue StandardError
15
+ false
16
+ end &&
14
17
  hash.dig(*action_path_arr) == true
15
18
  end
16
19
 
17
20
  def transform_namespace(namespace, current_path: nil)
18
21
  HashWithIndifferentAccess.new.tap do |transformed|
19
22
  namespace.each_pair do |key, value|
20
- raise InvalidNamespace,
21
- "`#{[current_path, key].compact.join(".")}` should be an array" unless value.is_a? Array
23
+ if !value.is_a? Array
24
+ raise InvalidNamespace,
25
+ "`#{[current_path, key].compact.join(".")}` should be an array"
26
+ end
27
+
28
+ if key.to_s.include?(".")
29
+ raise InvalidNamespace, "namespace or action should not contain period: `#{key}`"
30
+ end
22
31
 
23
32
  value.each.with_index do |arr_v, arr_i|
24
- if arr_v.is_a?(Symbol)
33
+ case arr_v
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
+
25
39
  transformed[key] ||= ::HashWithIndifferentAccess.new
26
- if transformed[key].has_key? arr_v
40
+ if transformed[key].key? arr_v
27
41
  raise InvalidNamespace, "duplicate entry: `#{[current_path, key, arr_v].compact.join(".")}`"
28
42
  end
29
43
 
30
44
  transformed[key][arr_v] = false
31
- elsif arr_v.is_a?(Hash)
45
+ when Hash
32
46
  transform_namespace(arr_v,
33
47
  current_path: [current_path, key].compact.join(".")).each_pair do |ts_k, ts_v|
34
48
  transformed[key] ||= ::HashWithIndifferentAccess.new
35
- if transformed[key].has_key? ts_k
49
+ if transformed[key].key? ts_k
36
50
  raise InvalidNamespace, "duplicate entry: `#{[current_path, key, ts_k].compact.join(".")}`"
37
51
  end
38
52
 
@@ -56,13 +70,13 @@ module Permisi
56
70
  def __deeply_sanitize_permissions(permission_hash, template: {})
57
71
  HashWithIndifferentAccess.new.tap do |sanitized|
58
72
  permission_hash.each_pair do |key, value|
59
- next unless template.has_key?(key)
73
+ next unless template.key?(key)
60
74
 
61
- if value.is_a?(Hash)
62
- sanitized[key] = __deeply_sanitize_permissions(value, template: template[key])
63
- else
64
- sanitized[key] = __cast_value_to_boolean(value)
65
- end
75
+ sanitized[key] = if value.is_a?(Hash)
76
+ __deeply_sanitize_permissions(value, template: template[key])
77
+ else
78
+ __cast_value_to_boolean(value)
79
+ end
66
80
  end
67
81
  end
68
82
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Permisi
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.5"
5
5
  end
data/permisi.gemspec CHANGED
@@ -33,9 +33,9 @@ Gem::Specification.new do |spec|
33
33
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
34
  spec.require_paths = ["lib"]
35
35
 
36
- spec.add_dependency "activesupport", ">= 3.2.0"
37
- spec.add_dependency "activerecord", ">= 3.2.0"
38
36
  spec.add_dependency "activemodel", ">= 3.2.0"
37
+ spec.add_dependency "activerecord", ">= 3.2.0"
38
+ spec.add_dependency "activesupport", ">= 3.2.0"
39
39
  spec.add_dependency "zeitwerk", ["~> 2.4", ">= 2.4.2"]
40
40
 
41
41
  # For more information and examples about making a new gem, checkout our
metadata CHANGED
@@ -1,17 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: permisi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.5
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-15 00:00:00.000000000 Z
11
+ date: 2021-02-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: activesupport
14
+ name: activemodel
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
@@ -39,7 +39,7 @@ dependencies:
39
39
  - !ruby/object:Gem::Version
40
40
  version: 3.2.0
41
41
  - !ruby/object:Gem::Dependency
42
- name: activemodel
42
+ name: activesupport
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - ">="
@@ -82,12 +82,13 @@ executables: []
82
82
  extensions: []
83
83
  extra_rdoc_files: []
84
84
  files:
85
- - ".github/workflows/main.yml"
85
+ - ".github/workflows/ci.yml"
86
86
  - ".gitignore"
87
87
  - ".rspec"
88
88
  - ".rubocop.yml"
89
89
  - CHANGELOG.md
90
90
  - CODE_OF_CONDUCT.md
91
+ - CONTRIBUTING.md
91
92
  - Gemfile
92
93
  - Gemfile.lock
93
94
  - LICENSE.txt
@@ -100,12 +101,11 @@ files:
100
101
  - lib/generators/permisi/templates/migration.rb
101
102
  - lib/permisi.rb
102
103
  - lib/permisi/actable.rb
104
+ - lib/permisi/backend.rb
103
105
  - lib/permisi/backend/active_record.rb
104
106
  - lib/permisi/backend/active_record/actor.rb
105
107
  - lib/permisi/backend/active_record/actor_role.rb
106
108
  - lib/permisi/backend/active_record/role.rb
107
- - lib/permisi/backend/base.rb
108
- - lib/permisi/backend/invalid_backend.rb
109
109
  - lib/permisi/backend/mongoid.rb
110
110
  - lib/permisi/config.rb
111
111
  - lib/permisi/permission_util.rb
@@ -1,18 +0,0 @@
1
- name: Ruby
2
-
3
- on: [push,pull_request]
4
-
5
- jobs:
6
- build:
7
- runs-on: ubuntu-latest
8
- steps:
9
- - uses: actions/checkout@v2
10
- - name: Set up Ruby
11
- uses: ruby/setup-ruby@v1
12
- with:
13
- ruby-version: 3.0.0
14
- - name: Run the default task
15
- run: |
16
- gem install bundler -v 2.2.5
17
- bundle install
18
- bundle exec rake
@@ -1,6 +0,0 @@
1
- module Permisi
2
- module Backend
3
- class Base
4
- end
5
- end
6
- end
@@ -1,9 +0,0 @@
1
- module Permisi
2
- module Backend
3
- class InvalidBackend < StandardError
4
- def initialize(message = "Please specify a backend. For details, check https://github.com/ukazap/permisi#configuring-backend")
5
- super
6
- end
7
- end
8
- end
9
- end