permisi 0.0.1 → 0.1.4

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: f4321f19a7a5c2ecbfa99d51c00a82d7fb1861007f52e6eab462c49df2c5b390
4
- data.tar.gz: ed73932d3a013e89a3d6c49c79c3328fe84ecfb566f66cd53ac1a9d355de383c
3
+ metadata.gz: 4b56c15224ab819fd173b51ce3cd341836a9edcbf6d2a0c0496813826c168f4c
4
+ data.tar.gz: 1eb2ef335b4fa1a2790573ca118c9178cff6d96ef6d3334cc165650c216bbc97
5
5
  SHA512:
6
- metadata.gz: a9ada7b5c95dcfa0a9627a7606bf4909e516f2e291aac518f61d1cf1a422e904027bf6c95910fc7ddde9b47c61e88d2027e0e377261af06e0d2dac16dd78e43d
7
- data.tar.gz: 421d311e27eb1f2663c6515c62623c3804ee77ee76bec16162b199370eb1619a4283acf5a79ae8eaca86da6daf7f32f5324c872ce9acc92a07b120f29cebe00c
6
+ metadata.gz: 9e099b2453555f7da80bfef2c4948a80523625271ba9666d2c053e455b0c42dc90d53f7f4a7233e1572696d58c7f1553dbdcaf38f9a4d6e351432a98de21d3ae
7
+ data.tar.gz: ea49a2fa692188d5864da2322a37c01de5856fb3d5bb4529966328f45e2ccc0114119c07289fa12c68afffc7f9e2030db99a10815480f180f3ea17fccd082218
@@ -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/.gitignore CHANGED
@@ -1,3 +1,4 @@
1
+ /spec/support/db/*.db
1
2
  /.bundle/
2
3
  /.yardoc
3
4
  /_yardoc/
@@ -9,3 +10,6 @@
9
10
 
10
11
  # rspec failure tracking
11
12
  .rspec_status
13
+
14
+ *.sublime-*
15
+ .byebug_history
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,4 +1,47 @@
1
- 0.0.0
2
- =====
1
+ # Changelog
3
2
 
4
- - Reserve gem name.
3
+ # 0.1.4
4
+
5
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.4/README.md)
6
+
7
+ - 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`
8
+ - Show warning when calling "roles.delete" because it won't invalidate the cache
9
+
10
+ # 0.1.3
11
+
12
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.3/README.md)
13
+
14
+ - Correct grammars and examples in the docs
15
+ - Change actor permissions cache key
16
+
17
+ # 0.1.2
18
+
19
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.2/README.md)
20
+
21
+ - Fix namespaces/actions should no longer contain periods
22
+ - Implement cache config for faster access to actor permissions
23
+
24
+ # 0.1.1
25
+
26
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.1/README.md)
27
+
28
+ - General code refactoring
29
+ - Improvements on ActiveRecord backend:
30
+ - Code refactoring
31
+ - Implement cache invalidation
32
+
33
+ # 0.1.0
34
+
35
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.1.0/README.md)
36
+
37
+ Finished extraction work from my past projects.
38
+
39
+ - Implement ActiveRecord backend
40
+ - Implement `Actable` mixin
41
+ - Implement permissions hash sanitization and checking
42
+
43
+ # 0.0.1
44
+
45
+ [_View the docs._](https://github.com/ukazap/permisi/blob/v0.0.1/README.md)
46
+
47
+ 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
@@ -5,8 +5,10 @@ source "https://rubygems.org"
5
5
  # Specify your gem's dependencies in permisi.gemspec
6
6
  gemspec
7
7
 
8
+ gem "byebug", "~> 11.1"
9
+ gem "codecov", "~> 0.4.3"
8
10
  gem "rake", "~> 13.0"
9
-
10
11
  gem "rspec", "~> 3.0"
11
-
12
- gem "rubocop", "~> 1.7"
12
+ gem "rubocop", "~> 1.9"
13
+ gem "simplecov", "~> 0.21.2"
14
+ gem "sqlite3", "~> 1.4"
data/Gemfile.lock CHANGED
@@ -1,13 +1,36 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- permisi (0.0.0)
4
+ permisi (0.1.4)
5
+ activemodel (>= 3.2.0)
6
+ activerecord (>= 3.2.0)
7
+ activesupport (>= 3.2.0)
8
+ zeitwerk (~> 2.4, >= 2.4.2)
5
9
 
6
10
  GEM
7
11
  remote: https://rubygems.org/
8
12
  specs:
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
+ concurrent-ruby (~> 1.0, >= 1.0.2)
20
+ i18n (>= 1.6, < 2)
21
+ minitest (>= 5.1)
22
+ tzinfo (~> 2.0)
23
+ zeitwerk (~> 2.3)
9
24
  ast (2.4.2)
25
+ byebug (11.1.3)
26
+ codecov (0.4.3)
27
+ simplecov (>= 0.15, < 0.22)
28
+ concurrent-ruby (1.1.8)
10
29
  diff-lcs (1.4.4)
30
+ docile (1.3.5)
31
+ i18n (1.8.9)
32
+ concurrent-ruby (~> 1.0)
33
+ minitest (5.14.3)
11
34
  parallel (1.20.1)
12
35
  parser (3.0.0.0)
13
36
  ast (~> 2.4.1)
@@ -28,7 +51,7 @@ GEM
28
51
  diff-lcs (>= 1.2.0, < 2.0)
29
52
  rspec-support (~> 3.10.0)
30
53
  rspec-support (3.10.2)
31
- rubocop (1.9.1)
54
+ rubocop (1.10.0)
32
55
  parallel (~> 1.10)
33
56
  parser (>= 3.0.0.0)
34
57
  rainbow (>= 2.2.2, < 4.0)
@@ -40,16 +63,30 @@ GEM
40
63
  rubocop-ast (1.4.1)
41
64
  parser (>= 2.7.1.5)
42
65
  ruby-progressbar (1.11.0)
66
+ simplecov (0.21.2)
67
+ docile (~> 1.1)
68
+ simplecov-html (~> 0.11)
69
+ simplecov_json_formatter (~> 0.1)
70
+ simplecov-html (0.12.3)
71
+ simplecov_json_formatter (0.1.2)
72
+ sqlite3 (1.4.2)
73
+ tzinfo (2.0.4)
74
+ concurrent-ruby (~> 1.0)
43
75
  unicode-display_width (2.0.0)
76
+ zeitwerk (2.4.2)
44
77
 
45
78
  PLATFORMS
46
79
  x86_64-linux
47
80
 
48
81
  DEPENDENCIES
82
+ byebug (~> 11.1)
83
+ codecov (~> 0.4.3)
49
84
  permisi!
50
85
  rake (~> 13.0)
51
86
  rspec (~> 3.0)
52
- rubocop (~> 1.7)
87
+ rubocop (~> 1.9)
88
+ simplecov (~> 0.21.2)
89
+ sqlite3 (~> 1.4)
53
90
 
54
91
  BUNDLED WITH
55
92
  2.2.5
data/README.md CHANGED
@@ -1,10 +1,46 @@
1
- # Permisi
2
-
3
- [![Gem Version](https://badge.fury.io/rb/permisi.svg)](https://badge.fury.io/rb/permisi)
4
-
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/permisi`. To experiment with that code, run `bin/console` for an interactive prompt.
6
-
7
- TODO: Delete this and the text above, and describe your gem
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
+
3
+ <table>
4
+ <tr>
5
+ <th>
6
+ <a href="https://commons.wikimedia.org/wiki/File:Female_Chinese_Lion_Statue.jpg">
7
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/17/Female_Chinese_Lion_Statue.jpg/102px-Female_Chinese_Lion_Statue.jpg">
8
+ </a>
9
+ </th>
10
+ <th>
11
+ <h1>Permisi</h1>
12
+ <p><em>Simple and dynamic role-based access control for Rails</em></p>
13
+ <p>
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>
23
+ </p>
24
+ </th>
25
+ <th>
26
+ <a href="https://commons.wikimedia.org/wiki/File:Male_Chinese_Lion_Statue.jpg">
27
+ <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/1/14/Male_Chinese_Lion_Statue.jpg/98px-Male_Chinese_Lion_Statue.jpg">
28
+ </a>
29
+ </th>
30
+ </tr>
31
+ </table>
32
+
33
+ ## Concept
34
+
35
+ Permisi provides a way of dynamically declaring user rights (a.k.a. permissions) using a simple role-based access control scheme.
36
+
37
+ This is not an alternative to CanCanCan/Pundit, instead it complement them with dynamic role definition and role membership.
38
+
39
+ Permisi has three basic concepts:
40
+
41
+ - Actor: a person, group of people, or an automated agent who interacts with the app
42
+ - Role: a job function, job title, or rank which determines an actor's authority
43
+ - Permission: the ability to perform an action
8
44
 
9
45
  ## Installation
10
46
 
@@ -17,24 +53,197 @@ gem 'permisi'
17
53
  And then execute:
18
54
 
19
55
  $ bundle install
56
+ $ rails g permisi:install
57
+
58
+ ## Configuring backend
59
+
60
+ Set `config.backend` in the initializer to the backend of choice for storing and retrieving roles:
61
+
62
+ ```ruby
63
+ # config/initializers/permisi.rb
64
+
65
+ Permisi.init do |config|
66
+ #...
67
+ config.backend = :active_record
68
+ #...
69
+ end
70
+ ```
71
+
72
+ To use `:active_record`, run the generated migration from the installation step:
73
+
74
+ $ rails db:migrate
75
+
76
+ Permisi only support `:active_record` backend at the moment. In the future, it will be possible to use `:mongoid`.
77
+
78
+ ## Configuring permissions
79
+
80
+ First you have to predefine the permissions, which is basically a set of possible actions according to the app's use cases. The actions can be grouped in any way possible. For example, you might want to define actions around resource types.
20
81
 
21
- Or install it yourself as:
82
+ To define the available actions in the system, assign a hash to the `config.permissions` with the following format:
22
83
 
23
- $ gem install permisi
84
+ ```ruby
85
+ # config/initializers/permisi.rb
86
+ Permisi.init do |config|
87
+ # ...
88
+ config.permissions = {
89
+ # A symbol-array pair denotes a namespace.
90
+ # A common use of namespacing is for grouping
91
+ # available actions by resources.
92
+ authors: [
93
+ # Enclosed in the array are symbols
94
+ # denoting available actions in the namespace:
95
+ :list,
96
+ :view,
97
+ :create,
98
+ :edit,
99
+ :delete
100
+ ],
101
+ # You can also use the simplified %i[] notation:
102
+ publishers: %i[list view create edit delete],
103
+ # Besides actions, you can also have nested
104
+ # namespaces:
105
+ books: [
106
+ :list,
107
+ :view,
108
+ :create,
109
+ :edit,
110
+ :delete,
111
+ {
112
+ editions: [
113
+ :list, :view, :create, :edit, :delete, :archive
114
+ ]
115
+ }
116
+ ]
117
+ }
118
+ # ...
119
+ end
120
+ ```
121
+
122
+ ## Defining and managing roles
123
+
124
+ Once you have the predefined permissions, you can then define different roles with different level of access within the boundary of the predefined permissions. You can delete or create new roles according to organizational changes. You can also modify existing roles without a change in your code.
125
+
126
+ You can create, edit, and destroy roles at runtime. You might also want to define preset roles via `db/seeds.rb`.
127
+
128
+ ```ruby
129
+ # Interact with Permisi.roles as you would with ActiveRecord query interfaces:
130
+
131
+ # List all roles
132
+ Permisi.roles.all
133
+
134
+ # Create a new role
135
+ admin_role = Permisi.roles.create(slug: :admin, name: "Administrator", permissions: {
136
+ books: {
137
+ list: true,
138
+ view: true,
139
+ create: true,
140
+ edit: true
141
+ }
142
+ })
143
+
144
+ # Ask specific role permission
145
+ admin_role.allows?("books.delete") # == false
146
+
147
+ # Update existing role
148
+ admin_role.permissions[:books].merge!({ delete: true })
149
+ admin_role.save
150
+ admin_role.allows?("books.delete") # == true
151
+ ```
152
+
153
+ ## Configuring actors
154
+
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.
156
+
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.
158
+
159
+ For example, you can map your user model to Permisi's actor model like so:
160
+
161
+ ```ruby
162
+ # app/models/user.rb
24
163
 
25
- ## Usage
164
+ class User < ApplicationRecord
165
+ include Permisi::Actable
166
+ end
167
+ ```
168
+
169
+ You can then interact using `#permisi` method:
170
+
171
+ ```ruby
172
+ user = User.find_by_email "esther@example.com"
173
+ user.permisi # => instance of Actor
174
+
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.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
+ ```
26
233
 
27
- TODO: Write usage instructions here
234
+ You can also roll your own [custom cache store](https://guides.rubyonrails.org/caching_with_rails.html#custom-cache-stores).
28
235
 
29
- ## Development
236
+ ### Cache/memo invalidation
30
237
 
31
- 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:
32
239
 
33
- 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
34
243
 
35
244
  ## Contributing
36
245
 
37
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/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/[USERNAME]/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).
38
247
 
39
248
  ## License
40
249
 
@@ -42,4 +251,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
42
251
 
43
252
  ## Code of Conduct
44
253
 
45
- 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/[USERNAME]/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).
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/migration"
5
+ require "rails/generators/active_record"
6
+
7
+ module Permisi
8
+ module Generators
9
+ class InstallGenerator < Rails::Generators::Base
10
+ include Rails::Generators::Migration
11
+
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def self.next_migration_number(path)
15
+ ActiveRecord::Generators::Base.next_migration_number(path)
16
+ end
17
+
18
+ def create_initializer
19
+ template "initializer.rb", "config/initializers/permisi.rb"
20
+ end
21
+
22
+ def create_migrations
23
+ migration_template "migration.rb", "db/migrate/create_permisi_tables.rb", migration_version: migration_version
24
+ end
25
+
26
+ private
27
+
28
+ def migration_version
29
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" if ActiveRecord.version.version > "5"
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "permisi"
4
+
5
+ Permisi.init do |config|
6
+ # Define which backend to use
7
+ # See https://github.com/ukazap/permisi#configuring-backend
8
+ config.backend = :active_record
9
+
10
+ # Define all permissions available in the system
11
+ # See https://github.com/ukazap/permisi#configuring-permissions
12
+ config.permissions = {}
13
+
14
+ # Define cache store
15
+ # See https://github.com/ukazap/permisi#caching
16
+ config.cache_store = Rails.cache
17
+ end
@@ -0,0 +1,30 @@
1
+ class CreatePermisiTables < ActiveRecord::Migration<%= migration_version %>
2
+ def up
3
+ create_table :permisi_actors do |t|
4
+ t.references :aka, polymorphic: true
5
+ t.timestamps
6
+ end
7
+
8
+ add_index :permisi_actors, [:aka_type, :aka_id]
9
+
10
+ create_table :permisi_roles do |t|
11
+ t.string :slug, null: false, unique: true
12
+ t.string :name, null: false, unique: true
13
+ t.json :permissions
14
+ t.timestamps
15
+ end
16
+
17
+ create_table :permisi_actor_roles do |t|
18
+ t.belongs_to :actor
19
+ t.belongs_to :role
20
+ end
21
+
22
+ add_index :permisi_actor_roles, [:actor_id, :role_id], unique: true
23
+ end
24
+
25
+ def down
26
+ drop_table :permisi_actor_roles
27
+ drop_table :permisi_roles
28
+ drop_table :permisi_actors
29
+ end
30
+ end
data/lib/permisi.rb CHANGED
@@ -1,8 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "permisi/version"
3
+ require "active_model/type"
4
+ require "active_support"
5
+ require "zeitwerk"
4
6
 
5
7
  module Permisi
6
- class Error < StandardError; end
7
- # Your code goes here...
8
+ LOADER = Zeitwerk::Loader.for_gem
9
+
10
+ class << self
11
+ def init
12
+ yield config if block_given?
13
+ end
14
+
15
+ def config
16
+ @config ||= Config.new
17
+ end
18
+
19
+ def actors
20
+ config.backend.actors
21
+ end
22
+
23
+ def actor(aka)
24
+ config.backend.findsert_actor(aka)
25
+ end
26
+
27
+ def roles
28
+ config.backend.roles
29
+ end
30
+ end
8
31
  end
32
+
33
+ Permisi::LOADER.ignore("#{__dir__}/generators")
34
+ Permisi::LOADER.ignore("#{__dir__}/permisi/backend/mongoid.rb") # todo
35
+ Permisi::LOADER.setup
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
4
+ module Actable
5
+ def permisi_actor
6
+ @permisi_actor ||= Permisi.actor(self)
7
+ end
8
+
9
+ alias permisi permisi_actor
10
+ end
11
+ end
@@ -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
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_record"
4
+
5
+ module Permisi
6
+ module Backend
7
+ module ActiveRecord
8
+ class << self
9
+ def table_name_prefix
10
+ "permisi_"
11
+ end
12
+
13
+ def findsert_actor(aka)
14
+ Actor.find_or_create_by(aka: aka)
15
+ end
16
+
17
+ def actors
18
+ Actor.all
19
+ end
20
+
21
+ def roles
22
+ Role.all
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
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, -> { distinct }, through: :actor_roles
10
+
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
52
+
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
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
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
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
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
10
+
11
+ validates_presence_of :name, :slug
12
+ validates_uniqueness_of :name, :slug
13
+
14
+ after_initialize :set_default_permissions
15
+ before_validation :sanitize_attributes
16
+ after_update :touch_actor_roles
17
+
18
+ serialize :permissions, Permisi::PermissionUtil::Serializer
19
+
20
+ def allows?(action_path)
21
+ Permisi::PermissionUtil.allows?(permissions, action_path)
22
+ end
23
+
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
34
+
35
+ def touch_actor_roles
36
+ actor_roles.each(&:touch)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
4
+ module Backend
5
+ module Mongoid
6
+ raise "under construction"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
4
+ class Config
5
+ class InvalidCacheStore < StandardError; end
6
+
7
+ NULL_CACHE_STORE = ActiveSupport::Cache::NullStore.new
8
+
9
+ attr_reader :permissions, :default_permissions
10
+
11
+ def initialize
12
+ @permissions = ::HashWithIndifferentAccess.new
13
+ @default_permissions = ::HashWithIndifferentAccess.new
14
+ end
15
+
16
+ def backend=(chosen_backend)
17
+ chosen_backend = "::Permisi::Backend::#{chosen_backend.to_s.classify}".constantize if chosen_backend.is_a? Symbol
18
+
19
+ if chosen_backend == Backend::ActiveRecord && VERSION == "0.1.4"
20
+ warn <<~MESSAGE
21
+
22
+ WARNING: If you are upgrading from Permisi >v0.1.4, please create the following migration:
23
+ `add_index :permisi_actor_roles, [:actor_id, :role_id], unique: true`
24
+
25
+ MESSAGE
26
+ end
27
+
28
+ @backend = chosen_backend
29
+ rescue NameError
30
+ raise Backend::InvalidBackend
31
+ end
32
+
33
+ def backend
34
+ @backend || Backend::NullBackend
35
+ end
36
+
37
+ def permissions=(permissions_hash)
38
+ permissions_hash = HashWithIndifferentAccess.new(permissions_hash)
39
+ @default_permissions = PermissionUtil.transform_namespace(permissions_hash)
40
+ @permissions = permissions_hash
41
+ end
42
+
43
+ def cache_store=(cache_store)
44
+ raise InvalidCacheStore unless cache_store.respond_to?(:fetch)
45
+
46
+ @cache_store = cache_store
47
+ end
48
+
49
+ def cache_store
50
+ @cache_store || NULL_CACHE_STORE
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Permisi
4
+ module PermissionUtil
5
+ class InvalidNamespace < StandardError; end
6
+
7
+ class << self
8
+ def allows?(hash, action_path)
9
+ return false unless hash.is_a?(Hash)
10
+
11
+ action_path_arr = action_path.split(".")
12
+ begin
13
+ !Permisi.config.default_permissions.dig(*action_path_arr).nil?
14
+ rescue StandardError
15
+ false
16
+ end &&
17
+ hash.dig(*action_path_arr) == true
18
+ end
19
+
20
+ def transform_namespace(namespace, current_path: nil)
21
+ HashWithIndifferentAccess.new.tap do |transformed|
22
+ namespace.each_pair do |key, value|
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
31
+
32
+ value.each.with_index do |arr_v, arr_i|
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
+
39
+ transformed[key] ||= ::HashWithIndifferentAccess.new
40
+ if transformed[key].key? arr_v
41
+ raise InvalidNamespace, "duplicate entry: `#{[current_path, key, arr_v].compact.join(".")}`"
42
+ end
43
+
44
+ transformed[key][arr_v] = false
45
+ when Hash
46
+ transform_namespace(arr_v,
47
+ current_path: [current_path, key].compact.join(".")).each_pair do |ts_k, ts_v|
48
+ transformed[key] ||= ::HashWithIndifferentAccess.new
49
+ if transformed[key].key? ts_k
50
+ raise InvalidNamespace, "duplicate entry: `#{[current_path, key, ts_k].compact.join(".")}`"
51
+ end
52
+
53
+ transformed[key][ts_k] = ts_v
54
+ end
55
+ else
56
+ raise InvalidNamespace,
57
+ "`#{[current_path, key].compact.join(".")}[#{arr_i}]` should be a symbol or a hash"
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ def sanitize_permissions(permission_hash)
65
+ __deeply_sanitize_permissions(permission_hash, template: Permisi.config.default_permissions)
66
+ end
67
+
68
+ private
69
+
70
+ def __deeply_sanitize_permissions(permission_hash, template: {})
71
+ HashWithIndifferentAccess.new.tap do |sanitized|
72
+ permission_hash.each_pair do |key, value|
73
+ next unless template.key?(key)
74
+
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
80
+ end
81
+ end
82
+ end
83
+
84
+ def __cast_value_to_boolean(value)
85
+ bool = ActiveModel::Type::Boolean.new.cast(value)
86
+ bool ||= false
87
+ end
88
+ end
89
+
90
+ class Serializer
91
+ def self.dump(hash)
92
+ hash
93
+ end
94
+
95
+ def self.load(hash)
96
+ (hash.is_a?(Hash) ? hash : {}).with_indifferent_access
97
+ end
98
+ end
99
+ end
100
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Permisi
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.4"
5
5
  end
data/permisi.gemspec CHANGED
@@ -8,15 +8,21 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Ukaza Perdana"]
9
9
  spec.email = ["ukaza@hey.com"]
10
10
 
11
- spec.summary = "Simple and dynamic user roles and permissions scheme for Rails"
12
- spec.description = "Permisi provides a way of dynamically declaring user rights (a.k.a. permissions) using a simple role-based access control scheme. A user may be associated to multiple roles with a different set of rights in each role."
11
+ spec.summary = "Simple and dynamic role-based access control for Rails"
12
+
13
+ spec.description = <<~DESCRIPTION
14
+ Permisi provides a way of dynamically declaring user rights (a.k.a. permissions) using a simple role-based access control scheme.
15
+ A user may be associated to multiple roles with a different set of permissions in each role.
16
+ The roles and user-roles association can be dynamically defined and changed on runtime.
17
+ DESCRIPTION
18
+
13
19
  spec.homepage = "https://github.com/ukazap/permisi"
14
20
  spec.license = "MIT"
15
- spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
21
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.4.4")
16
22
 
17
23
  spec.metadata["homepage_uri"] = spec.homepage
18
24
  spec.metadata["source_code_uri"] = spec.homepage
19
- spec.metadata["changelog_uri"] = "https://github.com/ukazap/permisi/blob.main/CHANGELOG.md"
25
+ spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
20
26
 
21
27
  # Specify which files should be added to the gem when it is released.
22
28
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
@@ -27,8 +33,10 @@ Gem::Specification.new do |spec|
27
33
  spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
34
  spec.require_paths = ["lib"]
29
35
 
30
- # Uncomment to register a new dependency of your gem
31
- # spec.add_dependency "example-gem", "~> 1.0"
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
+ spec.add_dependency "zeitwerk", ["~> 2.4", ">= 2.4.2"]
32
40
 
33
41
  # For more information and examples about making a new gem, checkout our
34
42
  # guide at: https://bundler.io/guides/creating_gem.html
metadata CHANGED
@@ -1,30 +1,94 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: permisi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.4
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-04 00:00:00.000000000 Z
12
- dependencies: []
13
- description: Permisi provides a way of dynamically declaring user rights (a.k.a. permissions)
14
- using a simple role-based access control scheme. A user may be associated to multiple
15
- roles with a different set of rights in each role.
11
+ date: 2021-02-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activemodel
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: activerecord
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.2.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: activesupport
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 3.2.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 3.2.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: zeitwerk
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.4'
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: 2.4.2
65
+ type: :runtime
66
+ prerelease: false
67
+ version_requirements: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - "~>"
70
+ - !ruby/object:Gem::Version
71
+ version: '2.4'
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.4.2
75
+ description: |
76
+ Permisi provides a way of dynamically declaring user rights (a.k.a. permissions) using a simple role-based access control scheme.
77
+ A user may be associated to multiple roles with a different set of permissions in each role.
78
+ The roles and user-roles association can be dynamically defined and changed on runtime.
16
79
  email:
17
80
  - ukaza@hey.com
18
81
  executables: []
19
82
  extensions: []
20
83
  extra_rdoc_files: []
21
84
  files:
22
- - ".github/workflows/main.yml"
85
+ - ".github/workflows/ci.yml"
23
86
  - ".gitignore"
24
87
  - ".rspec"
25
88
  - ".rubocop.yml"
26
89
  - CHANGELOG.md
27
90
  - CODE_OF_CONDUCT.md
91
+ - CONTRIBUTING.md
28
92
  - Gemfile
29
93
  - Gemfile.lock
30
94
  - LICENSE.txt
@@ -32,7 +96,19 @@ files:
32
96
  - Rakefile
33
97
  - bin/console
34
98
  - bin/setup
99
+ - lib/generators/permisi/install_generator.rb
100
+ - lib/generators/permisi/templates/initializer.rb
101
+ - lib/generators/permisi/templates/migration.rb
35
102
  - lib/permisi.rb
103
+ - lib/permisi/actable.rb
104
+ - lib/permisi/backend.rb
105
+ - lib/permisi/backend/active_record.rb
106
+ - lib/permisi/backend/active_record/actor.rb
107
+ - lib/permisi/backend/active_record/actor_role.rb
108
+ - lib/permisi/backend/active_record/role.rb
109
+ - lib/permisi/backend/mongoid.rb
110
+ - lib/permisi/config.rb
111
+ - lib/permisi/permission_util.rb
36
112
  - lib/permisi/version.rb
37
113
  - permisi.gemspec
38
114
  homepage: https://github.com/ukazap/permisi
@@ -41,7 +117,7 @@ licenses:
41
117
  metadata:
42
118
  homepage_uri: https://github.com/ukazap/permisi
43
119
  source_code_uri: https://github.com/ukazap/permisi
44
- changelog_uri: https://github.com/ukazap/permisi/blob.main/CHANGELOG.md
120
+ changelog_uri: https://github.com/ukazap/permisi/blob/main/CHANGELOG.md
45
121
  post_install_message:
46
122
  rdoc_options: []
47
123
  require_paths:
@@ -50,7 +126,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
50
126
  requirements:
51
127
  - - ">="
52
128
  - !ruby/object:Gem::Version
53
- version: 2.4.0
129
+ version: 2.4.4
54
130
  required_rubygems_version: !ruby/object:Gem::Requirement
55
131
  requirements:
56
132
  - - ">="
@@ -60,5 +136,5 @@ requirements: []
60
136
  rubygems_version: 3.2.3
61
137
  signing_key:
62
138
  specification_version: 4
63
- summary: Simple and dynamic user roles and permissions scheme for Rails
139
+ summary: Simple and dynamic role-based access control for Rails
64
140
  test_files: []
@@ -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