bullet_train-roles 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.circleci/config.yml +110 -0
- data/.gitignore +13 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +9 -0
- data/Gemfile.lock +190 -0
- data/LICENSE.txt +21 -0
- data/README.md +164 -0
- data/Rakefile +15 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/bullet_train-roles.gemspec +43 -0
- data/lib/bullet_train/roles/version.rb +5 -0
- data/lib/bullet_train/roles.rb +11 -0
- data/lib/generators/bullet_train/roles/install/USAGE +11 -0
- data/lib/generators/bullet_train/roles/install/install_generator.rb +184 -0
- data/lib/generators/bullet_train/roles/install/templates/roles.yml +15 -0
- data/lib/models/role.rb +196 -0
- data/lib/roles/permit.rb +57 -0
- data/lib/roles/support.rb +89 -0
- metadata +221 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/bullet_train//roles/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "bullet_train-roles"
|
|
7
|
+
spec.version = Roles::VERSION
|
|
8
|
+
spec.authors = ["Prabin Poudel", "Andrew Culver"]
|
|
9
|
+
spec.email = %w[andrew.culver@gmail.com]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Yaml-backed ApplicationHash for CanCan Roles"
|
|
12
|
+
spec.description = "Yaml-backed ApplicationHash for CanCan Roles"
|
|
13
|
+
spec.homepage = "https://github.com/bullet-train-co/bullet_train-roles"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/bullet-train-co/bullet_train-roles"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/bullet-train-co/bullet_train-roles/blob/main/CHANGELOG.md"
|
|
20
|
+
|
|
21
|
+
# Specify which files should be added to the gem when it is released.
|
|
22
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
23
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
|
24
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
spec.require_paths = ["lib"]
|
|
28
|
+
|
|
29
|
+
spec.add_development_dependency "byebug", "~> 11.1.0"
|
|
30
|
+
spec.add_development_dependency "factory_bot_rails", "~> 6.2.0"
|
|
31
|
+
spec.add_development_dependency "knapsack_pro", "~> 3.1.0"
|
|
32
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
|
33
|
+
spec.add_development_dependency "pg", "~> 1.2.0"
|
|
34
|
+
spec.add_development_dependency "rails", "~> 7.0.0"
|
|
35
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
36
|
+
spec.add_development_dependency "standard", "~> 1.5.0"
|
|
37
|
+
|
|
38
|
+
spec.add_runtime_dependency "active_hash"
|
|
39
|
+
spec.add_runtime_dependency "activesupport"
|
|
40
|
+
spec.add_runtime_dependency "cancancan"
|
|
41
|
+
# For more information and examples about making a new gem, checkout our
|
|
42
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
|
43
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Description:
|
|
2
|
+
Install required configuration
|
|
3
|
+
|
|
4
|
+
Example:
|
|
5
|
+
rails generate bullet_train:roles:install
|
|
6
|
+
|
|
7
|
+
This will:
|
|
8
|
+
create config/models/roles.yml
|
|
9
|
+
create migration file to add role_ids to top level model
|
|
10
|
+
add include Role::Support to top level model
|
|
11
|
+
add permit line to app/models/ability.rb
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_record"
|
|
4
|
+
require "rails/generators"
|
|
5
|
+
|
|
6
|
+
module BulletTrain
|
|
7
|
+
module Roles
|
|
8
|
+
class InstallGenerator < Rails::Generators::Base
|
|
9
|
+
source_root File.expand_path("templates", __dir__)
|
|
10
|
+
|
|
11
|
+
def add_roles_config_file
|
|
12
|
+
puts("Creating role.yml inside config/models with default roles\n")
|
|
13
|
+
|
|
14
|
+
copy_file "roles.yml", "config/models/roles.yml"
|
|
15
|
+
|
|
16
|
+
puts("Success 🎉🎉\n\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def configure_models
|
|
20
|
+
json_data_type_identifier = find_json_data_type_identifier
|
|
21
|
+
|
|
22
|
+
top_level_model = ask("Which model do you want to use as a top-level model that represents Membership? (Defaults to Membership)") || "Membership"
|
|
23
|
+
|
|
24
|
+
generate_migration_to_add_role_ids(top_level_model, json_data_type_identifier)
|
|
25
|
+
|
|
26
|
+
include_role_support_in_top_level_model(top_level_model)
|
|
27
|
+
|
|
28
|
+
associated_model = ask("Which model/association of #{top_level_model} do you consider to be the Team? (Default to Team)") || "Team"
|
|
29
|
+
|
|
30
|
+
# Asks for a model we can setup some default permissions for.
|
|
31
|
+
# TODO: follow back Andrew on question about this in Slack
|
|
32
|
+
|
|
33
|
+
add_permit_to_ability_model(top_level_model, associated_model)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def ask(question)
|
|
39
|
+
puts(question)
|
|
40
|
+
|
|
41
|
+
answer = gets.chomp
|
|
42
|
+
|
|
43
|
+
return if answer.blank?
|
|
44
|
+
|
|
45
|
+
answer
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def db_adapter
|
|
49
|
+
allowed_adapter_types = %w[mysql sqlite postgresql]
|
|
50
|
+
|
|
51
|
+
adapter_name = ActiveRecord::Base.connection.adapter_name.downcase
|
|
52
|
+
|
|
53
|
+
if allowed_adapter_types.exclude?(adapter_name)
|
|
54
|
+
raise NotImplementedError, "'#{adapter_name}' is not supported!"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
adapter_name
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def find_json_data_type_identifier
|
|
61
|
+
adapter_name = db_adapter
|
|
62
|
+
|
|
63
|
+
case adapter_name
|
|
64
|
+
when "postgresql"
|
|
65
|
+
"jsonb"
|
|
66
|
+
else
|
|
67
|
+
"json"
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_in_file(file_location, line_to_match, content_to_add)
|
|
72
|
+
update_file_content = []
|
|
73
|
+
|
|
74
|
+
file_lines = File.readlines(file_location)
|
|
75
|
+
|
|
76
|
+
file_lines.each do |line|
|
|
77
|
+
line_pattern = line_to_match
|
|
78
|
+
updated_line = line
|
|
79
|
+
|
|
80
|
+
if line.include?(line_pattern)
|
|
81
|
+
trimmed_line = line.tr("\n", "")
|
|
82
|
+
|
|
83
|
+
updated_line = "#{trimmed_line}#{content_to_add}"
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
update_file_content.push(updated_line)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
File.write(file_location, update_file_content.join)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def add_default_value_to_migration(file_name, table_name)
|
|
93
|
+
file_location = Dir["db/migrate/*_#{file_name}.rb"].last
|
|
94
|
+
line_to_match = "add_column :#{table_name.downcase}, :role_ids"
|
|
95
|
+
content_to_add = ", default: []\n"
|
|
96
|
+
|
|
97
|
+
add_in_file(file_location, line_to_match, content_to_add)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def migration_file_exists?(file_name)
|
|
101
|
+
file_location = Dir["db/migrate/*_#{file_name}.rb"].last
|
|
102
|
+
|
|
103
|
+
file_location.present?
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def file_already_exists(message)
|
|
107
|
+
puts(message)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def generate_migration_to_add_role_ids(top_level_model, json_data_type_identifier)
|
|
111
|
+
top_level_model_table_name = top_level_model.underscore.pluralize
|
|
112
|
+
|
|
113
|
+
migration_file_name = "add_role_ids_to_#{top_level_model_table_name}"
|
|
114
|
+
|
|
115
|
+
return file_already_exists("Migration file already exists!!\n\n") if migration_file_exists?(migration_file_name)
|
|
116
|
+
|
|
117
|
+
puts("Generating migration to add role_ids to #{top_level_model}")
|
|
118
|
+
|
|
119
|
+
generate "migration", "#{migration_file_name} role_ids:#{json_data_type_identifier}"
|
|
120
|
+
|
|
121
|
+
add_default_value_to_migration(migration_file_name, top_level_model_table_name)
|
|
122
|
+
|
|
123
|
+
puts("Success 🎉🎉\n\n")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def line_exists_in_file?(file_location, line_to_compare)
|
|
127
|
+
file_lines = File.readlines(file_location)
|
|
128
|
+
|
|
129
|
+
file_lines.join.include?(line_to_compare)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def line_already_exists(message)
|
|
133
|
+
puts(message)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def remove_new_lines_and_spaces(line)
|
|
137
|
+
line.tr("\n", "").strip
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def include_role_support_in_top_level_model(model_name)
|
|
141
|
+
# converts 👇
|
|
142
|
+
# Membership to membership
|
|
143
|
+
# Admin::Membership to admin/membership
|
|
144
|
+
# TeamMember to team_member
|
|
145
|
+
file_location = "app/models/#{model_name.underscore}.rb"
|
|
146
|
+
line_to_match = "class #{model_name.classify} < ApplicationRecord"
|
|
147
|
+
content_to_add = "\n include Roles::Support\n"
|
|
148
|
+
|
|
149
|
+
puts("Adding 'include Roles::Support' to #{model_name}\n\n")
|
|
150
|
+
|
|
151
|
+
if line_exists_in_file?(file_location, content_to_add)
|
|
152
|
+
message = "'#{remove_new_lines_and_spaces(content_to_add)}' already exists in #{model_name}!!\n\n"
|
|
153
|
+
|
|
154
|
+
return line_already_exists(message)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
add_in_file(file_location, line_to_match, content_to_add)
|
|
158
|
+
|
|
159
|
+
puts("Success 🎉🎉\n\n")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def add_permit_to_ability_model(top_level_model, associated_model)
|
|
163
|
+
# TODO: need to make this more smart e.g.
|
|
164
|
+
# 1. should know what parameter is used for user e.g. def initialize(user)
|
|
165
|
+
# 2. what if code doesn't include "if user.present?", maybe we need to handle that, consult with Andrew
|
|
166
|
+
file_location = "app/models/ability.rb"
|
|
167
|
+
line_to_match = "if user.present?"
|
|
168
|
+
content_to_add = "\n permit user, through: :#{top_level_model.downcase.pluralize}, parent: :#{associated_model.downcase}\n"
|
|
169
|
+
|
|
170
|
+
puts("Adding 'permit user, through: :#{top_level_model.downcase}, parent: :#{associated_model.downcase}' to #{associated_model}\n\n")
|
|
171
|
+
|
|
172
|
+
if line_exists_in_file?(file_location, content_to_add)
|
|
173
|
+
message = "#{remove_new_lines_and_spaces(content_to_add)} already exists in #{associated_model}!!\n\n"
|
|
174
|
+
|
|
175
|
+
return line_already_exists(message)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
add_in_file(file_location, line_to_match, content_to_add)
|
|
179
|
+
|
|
180
|
+
puts("Success 🎉🎉\n\n")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
data/lib/models/role.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_hash"
|
|
4
|
+
|
|
5
|
+
class RemovingLastTeamAdminException < RuntimeError; end
|
|
6
|
+
|
|
7
|
+
class Role < ActiveYaml::Base
|
|
8
|
+
include ActiveYaml::Aliases
|
|
9
|
+
set_root_path "config/models"
|
|
10
|
+
set_filename "roles"
|
|
11
|
+
|
|
12
|
+
field :includes, default: []
|
|
13
|
+
field :models, default: []
|
|
14
|
+
field :manageable_roles, default: []
|
|
15
|
+
|
|
16
|
+
def self.admin
|
|
17
|
+
find_by_key("admin")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.default
|
|
21
|
+
find_by_key("default")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.includes(role_or_key)
|
|
25
|
+
role_key = role_or_key.is_a?(Role) ? role_or_key.key : role_or_key
|
|
26
|
+
|
|
27
|
+
role = Role.find_by_key(role_key)
|
|
28
|
+
|
|
29
|
+
return Role.all.select(&:assignable?) if role.default?
|
|
30
|
+
|
|
31
|
+
result = []
|
|
32
|
+
|
|
33
|
+
all.each do |role|
|
|
34
|
+
result << role if role.includes.include?(role_key)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
result
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def self.assignable
|
|
41
|
+
all.reject(&:default?)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.find(key)
|
|
45
|
+
all.find { |role| role.key == key }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# We don't want to ever use the automatically generated ids from ActiveYaml. These are created based on the order of objects in the yml file
|
|
49
|
+
# so if someone ever changed the order of that file around, they would really mess up the user permissions. Instead, we're using the key attribute.
|
|
50
|
+
def id
|
|
51
|
+
key
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def included_by
|
|
55
|
+
Role.includes(self)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# We need to search for memberships that have this role included directly OR any memberships that have this role _through_ it being included in another role they have
|
|
59
|
+
def key_plus_included_by_keys
|
|
60
|
+
# get direct parent roles
|
|
61
|
+
(included_by.map(&:key_plus_included_by_keys).flatten + [id]).uniq
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def default?
|
|
65
|
+
key == "default"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def admin?
|
|
69
|
+
key == "admin"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def included_roles
|
|
73
|
+
default_roles = []
|
|
74
|
+
|
|
75
|
+
default_roles << Role.default unless default?
|
|
76
|
+
|
|
77
|
+
(default_roles + includes.map { |included_key| Role.find_by_key(included_key) }).uniq.compact
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def manageable_by?(role_or_roles)
|
|
81
|
+
return true if default?
|
|
82
|
+
|
|
83
|
+
roles = role_or_roles.is_a?(Array) ? role_or_roles : [role_or_roles]
|
|
84
|
+
|
|
85
|
+
roles.each do |role|
|
|
86
|
+
return true if role.manageable_roles.include?(key)
|
|
87
|
+
|
|
88
|
+
role.included_roles.each do |included_role|
|
|
89
|
+
return true if manageable_by?([included_role])
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def assignable?
|
|
97
|
+
!default?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def ability_generator(user, through, parent)
|
|
101
|
+
models.each do |model_name, _|
|
|
102
|
+
ag = AbilityGenerator.new(self, model_name, user, through, parent)
|
|
103
|
+
yield(ag)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# The only purpose of this class is to allow developers to do things like:
|
|
108
|
+
# Membership.first.roles << Role.admin
|
|
109
|
+
# Membership.first.roles.delete Role.admin
|
|
110
|
+
# It basically makes the role_ids column act like a Rails `has_many through` collection - this includes automatically saving changes when you add or remove from the array.
|
|
111
|
+
class Collection < Array
|
|
112
|
+
def initialize(model, ary)
|
|
113
|
+
@model = model
|
|
114
|
+
|
|
115
|
+
super(ary)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def <<(role)
|
|
119
|
+
return true if include?(role)
|
|
120
|
+
|
|
121
|
+
role_ids = @model.role_ids
|
|
122
|
+
|
|
123
|
+
role_ids << role.id
|
|
124
|
+
|
|
125
|
+
@model.update(role_ids: role_ids)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def delete(role)
|
|
129
|
+
@model.role_ids -= [role.key]
|
|
130
|
+
|
|
131
|
+
@model.save
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
class AbilityGenerator
|
|
136
|
+
attr_reader :model
|
|
137
|
+
|
|
138
|
+
def initialize(role, model_name, user, through, parent_name)
|
|
139
|
+
begin
|
|
140
|
+
@model = model_name.constantize
|
|
141
|
+
rescue NameError
|
|
142
|
+
raise "#{model_name} model is used in `config/models/roles.yml` for the #{role.key} role but is not defined in `app/models`."
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
@role = role
|
|
146
|
+
@ability_data = role.models[model_name]
|
|
147
|
+
@through = through
|
|
148
|
+
@parent = user.send(through).reflect_on_association(parent_name)&.klass
|
|
149
|
+
@parent_ids = user.parent_ids_for(@role, @through, parent_name) if @parent
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def valid?
|
|
153
|
+
actions.present? && model.present? && condition.present?
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def actions
|
|
157
|
+
return @actions if @actions
|
|
158
|
+
|
|
159
|
+
actions = (@ability_data["actions"] if @ability_data.is_a?(Hash)) || @ability_data
|
|
160
|
+
|
|
161
|
+
actions = [actions] unless actions.is_a?(Array)
|
|
162
|
+
|
|
163
|
+
@actions = actions.map!(&:to_sym)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def possible_parent_associations
|
|
167
|
+
ary = @parent.to_s.split("::").map(&:underscore)
|
|
168
|
+
|
|
169
|
+
possibilities = []
|
|
170
|
+
current = nil
|
|
171
|
+
|
|
172
|
+
until ary.empty?
|
|
173
|
+
current = "#{ary.pop}#{"_" unless current.nil?}#{current}"
|
|
174
|
+
possibilities << current
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
possibilities.map(&:to_sym)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def condition
|
|
181
|
+
return @condition if @condition
|
|
182
|
+
|
|
183
|
+
return nil unless @parent_ids
|
|
184
|
+
|
|
185
|
+
if @model == @parent
|
|
186
|
+
return @condition = {id: @parent_ids}
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
parent_association = possible_parent_associations.find { |association| @model.method_defined? association }
|
|
190
|
+
|
|
191
|
+
return nil unless parent_association.present?
|
|
192
|
+
|
|
193
|
+
@condition = {parent_association => {id: @parent_ids}}
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
data/lib/roles/permit.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Roles
|
|
4
|
+
module Permit
|
|
5
|
+
def permit(user, through:, parent:, debug: false)
|
|
6
|
+
# Without this, you need to restart the server each time you make changes to the config/models/role.yml file
|
|
7
|
+
Role.reload(true) if Rails.env.development?
|
|
8
|
+
|
|
9
|
+
# When changing permissions during development, you may also want to do this on each request:
|
|
10
|
+
# User.update_all ability_cache: nil if Rails.env.development?
|
|
11
|
+
|
|
12
|
+
output = []
|
|
13
|
+
added_roles = Set.new
|
|
14
|
+
|
|
15
|
+
user.send(through).map(&:roles).flatten.uniq.each do |role|
|
|
16
|
+
unless added_roles.include?(role)
|
|
17
|
+
output << "########### ROLE: #{role.key}"
|
|
18
|
+
|
|
19
|
+
output += add_abilities_for(role, user, through, parent)
|
|
20
|
+
|
|
21
|
+
added_roles << role
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
role.included_roles.each do |included_role|
|
|
25
|
+
unless added_roles.include?(included_role)
|
|
26
|
+
output << "############# INCLUDED ROLE: #{included_role.key}"
|
|
27
|
+
|
|
28
|
+
output += add_abilities_for(included_role, user, through, parent)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
if debug
|
|
34
|
+
puts "###########################"
|
|
35
|
+
puts "Auto generated `ability.rb` content:"
|
|
36
|
+
puts output
|
|
37
|
+
puts "############################"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def add_abilities_for(role, user, through, parent)
|
|
42
|
+
output = []
|
|
43
|
+
|
|
44
|
+
role.ability_generator(user, through, parent) do |ag|
|
|
45
|
+
if ag.valid?
|
|
46
|
+
output << "can #{ag.actions}, #{ag.model}, #{ag.condition}"
|
|
47
|
+
|
|
48
|
+
can(ag.actions, ag.model, ag.condition)
|
|
49
|
+
else
|
|
50
|
+
output << "# #{ag.model} does not respond to #{parent} so we're not going to add an ability for the #{through} context"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
output
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support"
|
|
4
|
+
|
|
5
|
+
module Roles
|
|
6
|
+
module Support
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
class_methods do
|
|
10
|
+
def roles_only(*roles)
|
|
11
|
+
@allowed_roles = roles.map(&:to_sym)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def assignable_roles
|
|
15
|
+
return Role.assignable if @allowed_roles.nil?
|
|
16
|
+
|
|
17
|
+
Role.assignable.select { |role| @allowed_roles.include?(role.key.to_sym) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Note default_role is an ActiveRecord core class method so we need to use something else here
|
|
21
|
+
def default_roles
|
|
22
|
+
default_role = Role.default
|
|
23
|
+
|
|
24
|
+
return [default_role] if @allowed_roles.nil?
|
|
25
|
+
|
|
26
|
+
@allowed_roles.include?(default_role.key.to_sym) ? [default_role] : []
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
included do
|
|
31
|
+
validate :validate_roles
|
|
32
|
+
|
|
33
|
+
# This query will return records that have a role "included" in a different role they have.
|
|
34
|
+
# For example, if you do with_roles(editor) it will return admin users if the admin role includes the editor role
|
|
35
|
+
scope :with_roles, ->(roles) { where("#{table_name}.role_ids ?| array[:keys]", keys: roles.map(&:key_plus_included_by_keys).flatten.uniq.map(&:to_s)) }
|
|
36
|
+
|
|
37
|
+
# This query will return roles that include the given role. See with_roles above for details
|
|
38
|
+
scope :with_role, ->(role) { role.nil? ? all : with_roles([role]) }
|
|
39
|
+
scope :viewers, -> { where("#{table_name}.role_ids = ?", [].to_json) }
|
|
40
|
+
scope :editors, -> { with_role(Role.find_by_key("editor")) }
|
|
41
|
+
scope :admins, -> { with_role(Role.find_by_key("admin")) }
|
|
42
|
+
|
|
43
|
+
after_save :invalidate_cache
|
|
44
|
+
after_destroy :invalidate_cache
|
|
45
|
+
|
|
46
|
+
def validate_roles
|
|
47
|
+
self.role_ids = role_ids.select(&:present?)
|
|
48
|
+
|
|
49
|
+
return if @allowed_roles.nil?
|
|
50
|
+
|
|
51
|
+
roles.each do |role|
|
|
52
|
+
errors.add(:roles, :invalid) unless @allowed_roles.include?(role.key.to_sym)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def roles
|
|
57
|
+
Role::Collection.new(self, (self.class.default_roles + roles_without_defaults).compact.uniq)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def roles=(roles)
|
|
61
|
+
self.role_ids = roles.map(&:key)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def assignable_roles
|
|
65
|
+
roles.select(&:assignable?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def roles_without_defaults
|
|
69
|
+
role_ids.map { |role_id| Role.find(role_id) }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def manageable_roles
|
|
73
|
+
roles.map(&:manageable_roles).flatten.uniq.map { |role_key| Role.find_by_key(role_key) }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def can_manage_role?(role)
|
|
77
|
+
manageable_roles.include?(role)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def admin?
|
|
81
|
+
roles.select(&:admin?).any?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def invalidate_cache
|
|
85
|
+
user&.invalidate_ability_cache
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|