wnw_permissible 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7ca90d99e80a44396bc21e25216575907dfc883d3983243311fc5baaf4518eaf
4
+ data.tar.gz: 6c593a7651f009d836faccc7c8d53bed7db610c90133605e4579669354b5347c
5
+ SHA512:
6
+ metadata.gz: 2083bba551f6e5301bd57efcbf54c23e729c713e39243501e31d203705f911f5465500099e631a7fffbb13b947f7c864cf984e9b51a4404206f080769b990747
7
+ data.tar.gz: ee13414403c26292c008639ae600c4b670aa6b51e4d8bac87b0d977222d63cca73f444f03c5bee6e7b7b27cfae9099d58bbcf2fe832341761473055a31883d54
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2018 Working Not Working
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Permissible
2
+ Adds WNW-style authorization into a Rails app.
3
+
4
+ [![Gem Version](https://badge.fury.io/rb/wnw-permissible.svg)](https://badge.fury.io/rb/wnw-permissible)
5
+ [![Build Status](https://semaphoreci.com/api/v1/workingnotworking/wnw-permissible/branches/master/badge.svg)](https://semaphoreci.com/workingnotworking/wnw-permissible)
6
+
7
+ ## Installation
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'wnw-permissible', :require => 'permissible'
12
+ ```
13
+
14
+ And then execute:
15
+ ```bash
16
+ $ bundle
17
+ ```
18
+
19
+ Unfortunately the gem name "permissible" was already taken. :(
20
+
21
+ ## Usage
22
+ In your `User` model (or any model you want to have roles and permissions) include the Permissible concern:
23
+
24
+ ```ruby
25
+ class User < ApplicationRecord
26
+ include Permissible
27
+ end
28
+ ```
29
+
30
+ Now your `User` has roles, permissions and limits that you can check in your app. Create some roles and permissions:
31
+
32
+ ```ruby
33
+ role = Role.create :name => 'admin'
34
+ permission = Permission.create :name => 'access_admin'
35
+ role.permissions << permission
36
+ ```
37
+
38
+ ```ruby
39
+ user = User.first
40
+ user.admin? #=> true (with Rails logger warning)
41
+ user.can_access_admin? #=> true
42
+ user.has_role? 'admin' #=> true
43
+ user.role_names #=> ['admin']
44
+ user.permissions #=> [:access_admin]
45
+ ```
46
+
47
+ ### About Roles
48
+
49
+ Role checks take the form of `[role]?` where `[role]` is the name of your role, like `admin?`. A Rails logger warning is output when you check roles in this manner since it's a bad practice: you should check permissions that a user has, not their role. Permissions are fluid and can move from role to role, so really it shouldn't matter what their named roles are, it matters what they can do.
50
+
51
+ ### About Permissions
52
+
53
+ Permission checks take the form of "can_[permission]?" where `[permission]` is the name you give to the permission. So if you have a permission with a name of "access_admin" then you check for that permission with `can_access_admin?`
54
+
55
+ Permissions are additive. If you have a permission, it returns `true`. If you don't have a permission, it returns false. If one role has a permission and the other does not, the user has that permission. There is no way for one role to force a permission to `false` if some other role provides it.
56
+
57
+ ## Role Limits
58
+
59
+ In addition to permissions a role can also have limits on some aspect of your app. Consider a search engine where a guest can only perform 10 searches but an admin can perform an unlimited number.
60
+
61
+ First create an attribute in the `RoleLimit` model that contains the value:
62
+
63
+ ```ruby
64
+ # db/migrate/201806131200000_create_role_limit_searches.rb
65
+ class CreateRoleLimitSearches < ActiveRecord::Migration[5.0]
66
+ def change
67
+ add_column :role_limits, :searches, :string
68
+ end
69
+ end
70
+ ```
71
+
72
+ Note that the column is a `:string`. The value contained in the column will be serialized (in YAML) so that it can contain arbitrary data that Rails will convert back to native datatypes for us.
73
+
74
+ Now decorate `RoleLimit` to declare that your new attribute should be serialized:
75
+
76
+ ```ruby
77
+ # app/decorators/role_limit_decorator.rb
78
+ RoleLimit.class_eval do
79
+ serialize :searches
80
+ end
81
+ ```
82
+
83
+ Now add `RoleLimit` records for your roles and see how they work:
84
+
85
+ ```ruby
86
+ admin = Role.create :name => 'admin'
87
+ guest = Role.create :name => 'guest'
88
+ admin.role_limits.create :searches => Float::INFINITY
89
+ guest.role_limits.create :searches => 10
90
+
91
+ alice = User.create
92
+ alice.roles << admin
93
+ bob = User.create
94
+ bob.roles << guest
95
+
96
+ alice.limit(:searches) #=> Infinity
97
+ bob.limit(:searches) #=> 10
98
+ ```
99
+
100
+ ## License
101
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,27 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Permissible'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ require 'bundler/gem_tasks'
18
+
19
+ require 'rake/testtask'
20
+
21
+ Rake::TestTask.new(:test) do |t|
22
+ t.libs << 'test'
23
+ t.pattern = 'test/**/*_test.rb'
24
+ t.verbose = false
25
+ end
26
+
27
+ task default: :test
@@ -0,0 +1,7 @@
1
+ class Authorization < ApplicationRecord
2
+ belongs_to :authorizable, :polymorphic => true, :touch => true
3
+ belongs_to :role
4
+
5
+ validates :role, :presence => true
6
+ validates :authorizable, :presence => true
7
+ end
@@ -0,0 +1,94 @@
1
+ module Permissible
2
+ extend ActiveSupport::Concern
3
+
4
+ PERMISSIBLE_PERMISSION_METHOD_REGEX = /^can_(.*)\?$/
5
+ PERMISSIBLE_ROLE_METHOD_REGEX = /\?$/
6
+
7
+ included do
8
+ has_many :authorizations, :as => :authorizable, :dependent => :destroy
9
+ has_many :roles, :through => :authorizations
10
+ has_many :role_limits, :through => :roles
11
+ end
12
+
13
+ def method_missing(method_id, *args)
14
+ case
15
+ when _permission_match?(method_id)
16
+ return has_permission?(_method_to_permission_name(method_id))
17
+ when _role_match?(method_id)
18
+ _role_check_warning(method_id)
19
+ return has_role?(_method_to_role_name(method_id))
20
+ else
21
+ super
22
+ end
23
+ end
24
+
25
+ def respond_to_missing?(method_id, *args)
26
+ _permission_match?(method_id) or
27
+ _role_match?(method_id) or
28
+ super
29
+ end
30
+
31
+ # list of permissions granted by roles, as :symbols
32
+ def permissions
33
+ @permissions ||= _permission_names_for_roles(roles).sort
34
+ end
35
+
36
+ def has_permission?(name)
37
+ permissions.include?(name)
38
+ end
39
+
40
+ def has_role?(name)
41
+ roles.where(:name => name).any?
42
+ end
43
+
44
+ def role_names
45
+ roles.pluck(:name)
46
+ end
47
+
48
+ # since a user can have multiple roles, take the max value of their limits:
49
+ #
50
+ # user.limit(:searches) #=> 8
51
+ def limit(attribute)
52
+ role_limits.pluck(attribute).max
53
+ end
54
+
55
+ # is method in the form of `can_something?`
56
+ private def _permission_match?(name)
57
+ !!(name.to_s =~ PERMISSIBLE_PERMISSION_METHOD_REGEX)
58
+ end
59
+
60
+ # is method in the form of `role?`
61
+ private def _role_match?(name)
62
+ name.to_s.match(PERMISSIBLE_ROLE_METHOD_REGEX) and
63
+ Role.where(:name => name.to_s.chop).any?
64
+ end
65
+
66
+ # converts a permission method check into a name: :can_do_this? => :do_this
67
+ private def _method_to_permission_name(name)
68
+ name.to_s.match(PERMISSIBLE_PERMISSION_METHOD_REGEX)[1].to_sym
69
+ end
70
+
71
+ # convers a role method check into a name: :admin? => 'admin'
72
+ private def _method_to_role_name(name)
73
+ name.to_s.chop
74
+ end
75
+
76
+ # Outputs a warning in the Rails log if a check for a user's role is found.
77
+ # The best practice is to only check if someone can do something, not if they
78
+ # have a certain title:
79
+ # http://programmers.stackexchange.com/questions/299729/role-vs-permission-based-access-control
80
+ private def _role_check_warning(method_id)
81
+ if Rails.env.development? or Rails.env.test?
82
+ position = caller.at(1).sub(%r{.*/},'').sub(%r{:in\s.*},'')
83
+ Rails.logger.warn "\033[0;33m WARNING: You should really be checking for individual permissions, not roles! #{self.class.name.to_s}##{method_id.to_s} called from #{position}"
84
+ end
85
+ end
86
+
87
+ # returns an array of symbols with the permission names granted by the given
88
+ # role(s)
89
+ private def _permission_names_for_roles(roles)
90
+ RolePermission.joins(:permission).where(:role_id => roles).distinct
91
+ .pluck(Permission.arel_table[:name]).collect(&:to_sym)
92
+ end
93
+
94
+ end
@@ -0,0 +1,6 @@
1
+ class Permission < ApplicationRecord
2
+ has_many :role_permissions, :dependent => :destroy
3
+ has_many :roles, :through => :role_permissions
4
+
5
+ validates :name, :presence => true, :uniqueness => true
6
+ end
@@ -0,0 +1,16 @@
1
+ class Role < ApplicationRecord
2
+ has_many :role_permissions, :dependent => :destroy
3
+ has_many :permissions, :through => :role_permissions
4
+ has_many :authorizations, :dependent => :destroy
5
+ has_many :role_limits, :dependent => :destroy
6
+
7
+ validates :name, :presence => true, :uniqueness => true
8
+
9
+ def limit(attribute)
10
+ role_limits.pluck(attribute).max
11
+ end
12
+
13
+ def human_name
14
+ name.split('_').last.titlecase
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ class RoleLimit < ApplicationRecord
2
+ belongs_to :role
3
+ end
@@ -0,0 +1,7 @@
1
+ class RolePermission < ApplicationRecord
2
+ belongs_to :role
3
+ belongs_to :permission
4
+
5
+ validates :role, :presence => true
6
+ validates :permission, :presence => true
7
+ end
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Generates (but does not run) a migration to add required tables.
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require "rails/generators/active_record"
5
+
6
+ module WnwPermissible
7
+ # Installs PaperTrail in a rails app.
8
+ class InstallGenerator < ::Rails::Generators::Base
9
+ include ::Rails::Generators::Migration
10
+
11
+ MODELS = ['authorization', 'permission', 'role', 'role_limit', 'role_permission']
12
+
13
+ source_root File.expand_path("templates", __dir__)
14
+ class_option(
15
+ :with_changes,
16
+ :type => :boolean,
17
+ :default => false,
18
+ :desc => "Store changeset (diff) with each version"
19
+ )
20
+
21
+ desc "Generates (but does not run) a migration to add a required tables."
22
+
23
+ def create_model_files
24
+ MODELS.each do |model|
25
+ if File.exist? File.join(destination_root,'app','models',"#{model}.rb")
26
+ ::Kernel.warn "Migration already exists: #{model}.rb"
27
+ else
28
+ template "model.rb.erb", "app/models/#{model}.rb", :model => model
29
+ end
30
+ end
31
+ end
32
+
33
+ def create_migration_files
34
+ migration_dir = File.expand_path("db/migrate")
35
+
36
+ MODELS.each do |template|
37
+ template_name = "create_#{template.pluralize}"
38
+ if self.class.migration_exists?(migration_dir, template_name)
39
+ ::Kernel.warn "Migration already exists: #{template_name}"
40
+ else
41
+ migration_template(
42
+ "#{template_name}.rb.erb",
43
+ "db/migrate/#{template_name}.rb",
44
+ :migration_version => migration_version
45
+ )
46
+ end
47
+ end
48
+ end
49
+
50
+ def self.next_migration_number(dirname)
51
+ ::ActiveRecord::Generators::Base.next_migration_number(dirname)
52
+ end
53
+
54
+ def migration_version
55
+ major = ActiveRecord::VERSION::MAJOR
56
+ if major >= 5
57
+ "[#{major}.#{ActiveRecord::VERSION::MINOR}]"
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', 'authorization.rb').to_s
5
+
6
+ class Authorization
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,14 @@
1
+ class CreateAuthorizations < ActiveRecord::Migration<%= migration_version %>
2
+
3
+ def change
4
+ create_table :authorizations do |t|
5
+ t.references :authorizable, :polymorphic => true
6
+ t.references :role, :foreign_key => true
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :authorizations, [:authorizable_id, :authorizable_type]
12
+ end
13
+
14
+ end
@@ -0,0 +1,14 @@
1
+ class CreatePermissions < ActiveRecord::Migration<%= migration_version %>
2
+
3
+ def change
4
+ create_table :permissions do |t|
5
+ t.string :name
6
+ t.text :description
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :permissions, :name, :unique => true
12
+ end
13
+
14
+ end
@@ -0,0 +1,11 @@
1
+ class CreateRoleLimits < ActiveRecord::Migration<%= migration_version %>
2
+
3
+ def change
4
+ create_table :role_limits do |t|
5
+ t.references :role, :foreign_key => true
6
+
7
+ t.timestamps
8
+ end
9
+ end
10
+
11
+ end
@@ -0,0 +1,12 @@
1
+ class CreateRolePermissions < ActiveRecord::Migration<%= migration_version %>
2
+
3
+ def change
4
+ create_table :role_permissions do |t|
5
+ t.references :role, :foreign_key => true
6
+ t.references :permission, :foreign_key => true
7
+
8
+ t.timestamps
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,14 @@
1
+ class CreateRoles < ActiveRecord::Migration<%= migration_version %>
2
+
3
+ def change
4
+ create_table :roles do |t|
5
+ t.string :name
6
+ t.text :description
7
+
8
+ t.timestamps
9
+ end
10
+
11
+ add_index :roles, :name, :unique => true
12
+ end
13
+
14
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', '<%= config[:model] %>.rb').to_s
5
+
6
+ class <%= config[:model].camelize %>
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', 'permission.rb').to_s
5
+
6
+ class Permission
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', 'role.rb').to_s
5
+
6
+ class Role
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', 'role_limit.rb').to_s
5
+
6
+ class RoleLimit
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,8 @@
1
+ # Created by WnwPermissible to contain any custom model code. It's safe to
2
+ # delete this file if you do not need any custom functionality in the model.
3
+
4
+ require_dependency WnwPermissible::Engine.config.root.join('app', 'models', 'role_permission.rb').to_s
5
+
6
+ class RolePermission
7
+ # add custom model code here
8
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :permissible do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,11 @@
1
+ module WnwPermissible
2
+ class Engine < ::Rails::Engine
3
+
4
+ config.to_prepare do
5
+ Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
6
+ require_dependency(c)
7
+ end
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module WnwPermissible
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,5 @@
1
+ require "wnw_permissible/engine"
2
+
3
+ module WnwPermissible
4
+
5
+ end
metadata ADDED
@@ -0,0 +1,98 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: wnw_permissible
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Rob Cameron
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-06-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sqlite3
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ description: Include as a concern in your User model
42
+ email:
43
+ - rob@workingnotworking.com
44
+ executables: []
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - MIT-LICENSE
49
+ - README.md
50
+ - Rakefile
51
+ - app/models/authorization.rb
52
+ - app/models/concerns/permissible.rb
53
+ - app/models/permission.rb
54
+ - app/models/role.rb
55
+ - app/models/role_limit.rb
56
+ - app/models/role_permission.rb
57
+ - lib/generators/wnw_permissible/USAGE
58
+ - lib/generators/wnw_permissible/install_generator.rb
59
+ - lib/generators/wnw_permissible/templates/authorization.rb.erb
60
+ - lib/generators/wnw_permissible/templates/create_authorizations.rb.erb
61
+ - lib/generators/wnw_permissible/templates/create_permissions.rb.erb
62
+ - lib/generators/wnw_permissible/templates/create_role_limits.rb.erb
63
+ - lib/generators/wnw_permissible/templates/create_role_permissions.rb.erb
64
+ - lib/generators/wnw_permissible/templates/create_roles.rb.erb
65
+ - lib/generators/wnw_permissible/templates/model.rb.erb
66
+ - lib/generators/wnw_permissible/templates/permission.rb.erb
67
+ - lib/generators/wnw_permissible/templates/role.rb.erb
68
+ - lib/generators/wnw_permissible/templates/role_limit.rb.erb
69
+ - lib/generators/wnw_permissible/templates/role_permission.rb.erb
70
+ - lib/tasks/wnw_permissible_tasks.rake
71
+ - lib/wnw_permissible.rb
72
+ - lib/wnw_permissible/engine.rb
73
+ - lib/wnw_permissible/version.rb
74
+ homepage: https://github.com/workingnotworking/wnw_permissible
75
+ licenses:
76
+ - MIT
77
+ metadata: {}
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ required_rubygems_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project:
94
+ rubygems_version: 2.7.7
95
+ signing_key:
96
+ specification_version: 4
97
+ summary: Adds role-based permissions to a Rails application
98
+ test_files: []