bullet_train-roles 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|