active_security 1.0.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
- checksums.yaml.gz.sig +0 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/CONTRIBUTING.md +66 -0
- data/LICENSE.txt +21 -0
- data/README.md +263 -0
- data/SECURITY.md +13 -0
- data/lib/active_security/base.rb +194 -0
- data/lib/active_security/configuration.rb +107 -0
- data/lib/active_security/finder_methods.rb +64 -0
- data/lib/active_security/finders.rb +118 -0
- data/lib/active_security/privileged.rb +39 -0
- data/lib/active_security/privileged_hooks.rb +13 -0
- data/lib/active_security/restricted.rb +71 -0
- data/lib/active_security/restricted_hooks.rb +41 -0
- data/lib/active_security/scoped.rb +100 -0
- data/lib/active_security/version.rb +7 -0
- data/lib/active_security.rb +91 -0
- data.tar.gz.sig +0 -0
- metadata +394 -0
- metadata.gz.sig +0 -0
@@ -0,0 +1,194 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
# @guide begin
|
3
|
+
#
|
4
|
+
# ## Setting Up ActiveSecurity in Your Model
|
5
|
+
#
|
6
|
+
# To use ActiveSecurity in your ActiveRecord models, you must first either extend or
|
7
|
+
# include the ActiveSecurity module (it makes no difference), then invoke the
|
8
|
+
# {ActiveSecurity::Base#active_security active_security} method to configure your desired
|
9
|
+
# options:
|
10
|
+
#
|
11
|
+
# class Foo < ActiveRecord::Base
|
12
|
+
# include ActiveSecurity
|
13
|
+
# active_security :use => [:finders, :scoped], scope: :bar_id
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# The most important option is `:use`, which you use to tell ActiveSecurity which
|
17
|
+
# addons it should use. See the documentation for {ActiveSecurity::Base#active_security} for a list of all
|
18
|
+
# available addons, or skim through the rest of the docs to get a high-level
|
19
|
+
# overview.
|
20
|
+
#
|
21
|
+
# *A note about single table inheritance (STI): you must extend ActiveSecurity in*
|
22
|
+
# *all classes that participate in STI, both your parent classes and their*
|
23
|
+
# *children.*
|
24
|
+
#
|
25
|
+
# ### The Default Setup: Simple Models
|
26
|
+
#
|
27
|
+
# The simplest way to use ActiveSecurity is to have it ensure that finds are executed within a scope:
|
28
|
+
#
|
29
|
+
# class User < ActiveRecord::Base
|
30
|
+
# extend ActiveSecurity
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# User.restricted.find(1) # blows up, because no scope
|
34
|
+
# User.where(...).restricted.find(1) # returns the user
|
35
|
+
#
|
36
|
+
# ### The Strict Setup: Simple Models
|
37
|
+
#
|
38
|
+
# The problem with the above approach is that a naked find (`User.find(1)`)
|
39
|
+
# still works, and is just as insecure as before. The `:finders` plugin fixes
|
40
|
+
# this problem, so you don't need to add `restricted` everywhere.
|
41
|
+
#
|
42
|
+
# class User < ActiveRecord::Base
|
43
|
+
# extend ActiveSecurity
|
44
|
+
# active_security use: {finders: {default_finders: :restricted}}
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# User.find(1) # blows up, because no scope
|
48
|
+
# User.where(...).find(1) # returns the user
|
49
|
+
#
|
50
|
+
# @guide end
|
51
|
+
module Base
|
52
|
+
# Configure ActiveSecurity's behavior in a model.
|
53
|
+
#
|
54
|
+
# class Post < ActiveRecord::Base
|
55
|
+
# extend ActiveSecurity
|
56
|
+
# active_security use: :finders
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# When given the optional block, this method will yield the class's instance
|
60
|
+
# of {ActiveSecurity::Configuration} to the block before evaluating other
|
61
|
+
# arguments, so configuration values set in the block may be overwritten by
|
62
|
+
# the arguments. This order was chosen to allow passing the same proc to
|
63
|
+
# multiple models, while being able to override the values it sets. Here is
|
64
|
+
# a contrived example:
|
65
|
+
#
|
66
|
+
# $active_security_config_proc = Proc.new do |config|
|
67
|
+
# config.use :finders
|
68
|
+
# end
|
69
|
+
#
|
70
|
+
# class Foo < ActiveRecord::Base
|
71
|
+
# extend ActiveSecurity
|
72
|
+
# active_security &$active_security_config_proc
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# class Bar < ActiveRecord::Base
|
76
|
+
# extend ActiveSecurity
|
77
|
+
# active_security &$active_security_config_proc
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# However, it's usually better to use {ActiveSecurity.defaults} for this:
|
81
|
+
#
|
82
|
+
# ActiveSecurity.defaults do |config|
|
83
|
+
# config.use :finders, default_finders: :restricted
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# class Foo < ActiveRecord::Base
|
87
|
+
# extend ActiveSecurity
|
88
|
+
# end
|
89
|
+
#
|
90
|
+
# class Bar < ActiveRecord::Base
|
91
|
+
# extend ActiveSecurity
|
92
|
+
# end
|
93
|
+
#
|
94
|
+
# In general you should use the block syntax either because of your personal
|
95
|
+
# aesthetic preference, or because you need to share some functionality
|
96
|
+
# between multiple models that can't be well encapsulated by
|
97
|
+
# {ActiveSecurity.defaults}.
|
98
|
+
#
|
99
|
+
# ### Order Method Calls in a Block vs Ordering Options
|
100
|
+
#
|
101
|
+
# When calling this method without a block, you may set the hash options in
|
102
|
+
# any order.
|
103
|
+
#
|
104
|
+
# However, when using block-style invocation, be sure to call
|
105
|
+
# ActiveSecurity::Configuration's {ActiveSecurity::Configuration#use use} method
|
106
|
+
# *prior* to the associated configuration options, because it will include
|
107
|
+
# modules into your class, and these modules in turn may add required
|
108
|
+
# configuration options to the `@active_security_configuration`'s class:
|
109
|
+
#
|
110
|
+
# class Person < ActiveRecord::Base
|
111
|
+
# active_security do |config|
|
112
|
+
# # This will work
|
113
|
+
# config.use :scoped
|
114
|
+
# config.scope = "family_id"
|
115
|
+
# end
|
116
|
+
# end
|
117
|
+
#
|
118
|
+
# class Person < ActiveRecord::Base
|
119
|
+
# active_security do |config|
|
120
|
+
# # This will fail
|
121
|
+
# config.scope = "family_id"
|
122
|
+
# config.use :scoped
|
123
|
+
# end
|
124
|
+
# end
|
125
|
+
#
|
126
|
+
# ### Including Your Own Modules
|
127
|
+
#
|
128
|
+
# Because :use can accept a name or a Module, {ActiveSecurity.defaults defaults}
|
129
|
+
# can be a convenient place to set up behavior common to all classes using
|
130
|
+
# ActiveSecurity. You can include any module, or more conveniently, define one
|
131
|
+
# on-the-fly. For example, let's say you want to override the error that is
|
132
|
+
# raised when no scope is used:
|
133
|
+
#
|
134
|
+
# ActiveSecurity.defaults do |config|
|
135
|
+
# config.use :finders
|
136
|
+
# config.use Module.new {
|
137
|
+
# def self.setup(model_class)
|
138
|
+
# model_class.instance_eval do
|
139
|
+
# relation.class.send(:prepend, RaiseOverride)
|
140
|
+
# model_class.singleton_class.send(:prepend, RaiseOverride)
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# association_relation_delegate_class = model_class.relation_delegate_class(::ActiveRecord::AssociationRelation)
|
144
|
+
# association_relation_delegate_class.send(:prepend, RaiseOverride)
|
145
|
+
# end
|
146
|
+
#
|
147
|
+
# module RaiseOverride
|
148
|
+
# def raise_if_not_scoped
|
149
|
+
# puts "My errors are better than yours"
|
150
|
+
# raise StandardError, "Calm Down"
|
151
|
+
# end
|
152
|
+
# end
|
153
|
+
# }
|
154
|
+
# end
|
155
|
+
#
|
156
|
+
#
|
157
|
+
# @option options [Symbol,Module] :use The addon or name of an addon to use.
|
158
|
+
# By default, ActiveSecurity provides {ActiveSecurity::Finders :finders},
|
159
|
+
# {ActiveSecurity::Restricted :restricted}, {ActiveSecurity::Privileged :privileged},
|
160
|
+
# and {ActiveSecurity::Scoped :scoped}.
|
161
|
+
#
|
162
|
+
# @option options [Symbol] :scope Available when using `:scoped`.
|
163
|
+
# Sets the relation or column which will be considered a required scope.
|
164
|
+
# This option has no default value.
|
165
|
+
#
|
166
|
+
# @yield Provides access to the model class's active_security_config, which
|
167
|
+
# allows an alternate configuration syntax, and conditional configuration
|
168
|
+
# logic.
|
169
|
+
#
|
170
|
+
# @yieldparam config The model class's {ActiveSecurity::Configuration active_security_config}.
|
171
|
+
def active_security(options = {}, &block)
|
172
|
+
yield active_security_config if block
|
173
|
+
use_mods = options.delete(:use)
|
174
|
+
if use_mods
|
175
|
+
active_security_config.use(use_mods) do |config|
|
176
|
+
config.send(:set, options)
|
177
|
+
end
|
178
|
+
else
|
179
|
+
active_security_config.send(:set, options)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
# Returns the model class's {ActiveSecurity::Configuration active_security_config}.
|
184
|
+
# @note In the case of Single Table Inheritance (STI), this method will
|
185
|
+
# duplicate the parent class's ActiveSecurity::Configuration and relation class
|
186
|
+
# on first access. If you're concerned about thread safety, then be sure
|
187
|
+
# to invoke {#active_security} in your class for each model.
|
188
|
+
def active_security_config
|
189
|
+
@active_security_config ||= base_class.active_security_config.dup.tap do |config|
|
190
|
+
config.model_class = self
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
# The configuration parameters passed to {Base#active_security} will be stored in
|
3
|
+
# this object.
|
4
|
+
class Configuration
|
5
|
+
# The default configuration options.
|
6
|
+
attr_reader :defaults
|
7
|
+
|
8
|
+
# The modules in use
|
9
|
+
attr_reader :modules
|
10
|
+
|
11
|
+
# The model class that this configuration belongs to.
|
12
|
+
# @return ActiveRecord::Base
|
13
|
+
attr_accessor :model_class
|
14
|
+
|
15
|
+
# The module to use for finders
|
16
|
+
attr_accessor :finder_methods
|
17
|
+
|
18
|
+
# Where logs will be sent
|
19
|
+
attr_accessor :logger
|
20
|
+
|
21
|
+
def initialize(model_class, values = nil)
|
22
|
+
@model_class = model_class
|
23
|
+
@defaults = {}
|
24
|
+
@logger = ActiveRecord::Base.logger
|
25
|
+
@modules = []
|
26
|
+
@finder_methods = ActiveSecurity::FinderMethods
|
27
|
+
set(values)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Lets you specify the addon modules to use with ActiveSecurity.
|
31
|
+
#
|
32
|
+
# This method is invoked by {ActiveSecurity::Base#active_security active_security} when
|
33
|
+
# passing the `:use` option, or when using {ActiveSecurity::Base#active_security
|
34
|
+
# active_security} with a block.
|
35
|
+
#
|
36
|
+
# @example
|
37
|
+
# class Book < ActiveRecord::Base
|
38
|
+
# extend ActiveSecurity
|
39
|
+
# active_security use: :finders
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# @param [#to_s, Module, Hash[[#to_s, Module], Array[Hash[#to_s, any]]]] modules
|
43
|
+
# Arguments should be Modules, or symbols or
|
44
|
+
# strings that correspond with the name of an addon to use with ActiveSecurity,
|
45
|
+
# or a hash/array of hashes where the keys are Modules, symbols, or strings corresponding as previously described, and
|
46
|
+
# the values are the Hashes of key value pairs of configuration attributes and their assigned values.
|
47
|
+
# By default ActiveSecurity provides `:finders`, `:privileged`, `:restricted` and `:scoped`.
|
48
|
+
def use(*modules, &block)
|
49
|
+
mods = modules.to_a.compact
|
50
|
+
mods.map.with_index do |object, idx|
|
51
|
+
case object
|
52
|
+
when Array
|
53
|
+
object.each do |obj|
|
54
|
+
if obj.is_a?(Hash)
|
55
|
+
_handle_hash(obj)
|
56
|
+
else
|
57
|
+
mod = get_module(obj)
|
58
|
+
_use(mod, idx, &block)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
when Hash
|
62
|
+
_handle_hash(object)
|
63
|
+
when String, Symbol, Module
|
64
|
+
mod = get_module(object)
|
65
|
+
_use(mod, idx, &block)
|
66
|
+
else
|
67
|
+
raise InvalidConfig, "Unknown Argument Type #{object.class}: #{object.inspect}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def _handle_hash(hash)
|
73
|
+
hash.each do |mod_key, attrs|
|
74
|
+
mod = get_module(mod_key)
|
75
|
+
_use(mod, 0) do |config|
|
76
|
+
config.send(:set, attrs)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def _use(mod, idx, &block)
|
82
|
+
mod.setup(@model_class) if mod.respond_to?(:setup)
|
83
|
+
@model_class.send(:include, mod) unless uses?(mod)
|
84
|
+
# Only yield on the first module, so as to not run the block multiple times,
|
85
|
+
# and because later modules may require the attributes of a prior module to exist.
|
86
|
+
# The block structure won't work for more complex config than that.
|
87
|
+
# For more complex configuration pass a Hash where the keys are the "modules"
|
88
|
+
yield self if block_given? && idx.zero?
|
89
|
+
mod.after_config(@model_class) if mod.respond_to?(:after_config)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Returns whether the given module is in use.
|
93
|
+
def uses?(mod)
|
94
|
+
@model_class < get_module(mod)
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def get_module(object)
|
100
|
+
(Module === object) ? object : ActiveSecurity.const_get(object.to_s.titleize.camelize.gsub(/\s+/, ""))
|
101
|
+
end
|
102
|
+
|
103
|
+
def set(values)
|
104
|
+
values&.each { |name, value| send(:"#{name}=", value) }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
module FinderMethods
|
3
|
+
# Finds a record using the given id.
|
4
|
+
# Because this is injected into the Model as well as the relation,
|
5
|
+
# we need handling for both.
|
6
|
+
def find(*args)
|
7
|
+
if is_a?(ActiveRecord::Relation)
|
8
|
+
_active_security_enforce_if_not_scoped
|
9
|
+
else
|
10
|
+
_active_security_not_scoped_handler
|
11
|
+
end
|
12
|
+
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def _active_security_enforce_if_not_scoped
|
19
|
+
scoped_securely =
|
20
|
+
if active_security_config.respond_to?(:scope_columns)
|
21
|
+
values[:where].present? && active_security_config.scope_columns.all? do |scope_column|
|
22
|
+
predicates = values[:where].send(:predicates)
|
23
|
+
_active_security_check_predicates_for_scope(predicates, scope_column)
|
24
|
+
end
|
25
|
+
else
|
26
|
+
# If we don't have a specific scope requirement, then just ensure there is some scope.
|
27
|
+
values[:where].present?
|
28
|
+
end
|
29
|
+
_active_security_not_scoped_handler unless scoped_securely
|
30
|
+
end
|
31
|
+
|
32
|
+
def _active_security_check_predicates_for_scope(predicates, scope_column)
|
33
|
+
predicates.detect do |predicate|
|
34
|
+
# Consider alternative...
|
35
|
+
# comp =
|
36
|
+
# case predicate
|
37
|
+
# when Arel::Nodes::HomogeneousIn
|
38
|
+
# predicate.attribute.name
|
39
|
+
# when Arel::Nodes::Equality
|
40
|
+
# predicate.right.name
|
41
|
+
# end
|
42
|
+
comp =
|
43
|
+
if predicate.respond_to?(:attribute) && predicate.attribute.respond_to?(:name)
|
44
|
+
# Handles Arel::Nodes::HomogeneousIn
|
45
|
+
predicate.attribute.name
|
46
|
+
elsif predicate.respond_to?(:right) && predicate.right.respond_to?(:name)
|
47
|
+
# Handles Arel::Nodes::Equality
|
48
|
+
predicate.right.name
|
49
|
+
else
|
50
|
+
_active_security_unhandled_predicate(predicate)
|
51
|
+
end
|
52
|
+
comp == scope_column
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def _active_security_name_for
|
57
|
+
if respond_to?(name)
|
58
|
+
"(#{name})"
|
59
|
+
else
|
60
|
+
"(#{(self.class.name == "Class") ? to_s : self.class.name})"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
# @guide begin
|
3
|
+
#
|
4
|
+
# ## Performing Finds with ActiveSecurity
|
5
|
+
#
|
6
|
+
# ActiveSecurity offers enhanced finders which will search for your record while
|
7
|
+
# ensuring that a particular scope is present. This makes it easy
|
8
|
+
# to add ActiveSecurity to an existing application with minimal code modification.
|
9
|
+
#
|
10
|
+
# By default, these enhanced finders are available only on the `restricted` scope:
|
11
|
+
#
|
12
|
+
# Restaurant.restricted.find(23) #=> Will blow up, because no scope!
|
13
|
+
# Restaurant.find(23) #=> works
|
14
|
+
#
|
15
|
+
# ActiveSecurity overrides the default finder methods to perform
|
16
|
+
# secure finds all the time. This requires modifying parts of Rails that do
|
17
|
+
# not have a public API, which is hard to maintain and may cause
|
18
|
+
# compatibility issues.
|
19
|
+
#
|
20
|
+
# class Restaurant < ActiveRecord::Base
|
21
|
+
# extend ActiveSecurity
|
22
|
+
#
|
23
|
+
# scope :active, -> {where(active: true)}
|
24
|
+
#
|
25
|
+
# active_security use: [:finders]
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# Restaurant.restricted.find(23) #=> blows up, because no scope!
|
29
|
+
# Restaurant.find(23) #=> also blows up, because no scope!
|
30
|
+
# Restaurant.active.find(23) #=> works, because scoped!
|
31
|
+
# Restaurant.active.restricted.find(23) #=> also works, because scoped!
|
32
|
+
#
|
33
|
+
# ### Updating your application to use ActiveSecurity's finders
|
34
|
+
#
|
35
|
+
# Unless you've chosen to use the `:finders` addon, be sure to modify the finders
|
36
|
+
# in your controllers to use the `restricted` scope. For example:
|
37
|
+
#
|
38
|
+
# # before
|
39
|
+
# def set_restaurant
|
40
|
+
# @restaurant = Restaurant.find(params[:id])
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# # after
|
44
|
+
# def set_restaurant
|
45
|
+
# @restaurant = Restaurant.restricted.find(params[:id])
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# #### Active Admin
|
49
|
+
#
|
50
|
+
# Unless you use the `:finders` addon, you should modify your admin controllers
|
51
|
+
# for models that use ActiveSecurity with something similar to the following:
|
52
|
+
#
|
53
|
+
# controller do
|
54
|
+
# def find_resource
|
55
|
+
# scoped_collection.restricted.find(params[:id])
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
#
|
59
|
+
# @guide end
|
60
|
+
module Finders
|
61
|
+
class << self
|
62
|
+
# ActiveSecurity::Config.use will invoke this method when present, to allow
|
63
|
+
# loading dependent modules prior to overriding them when necessary.
|
64
|
+
def setup(model_class)
|
65
|
+
model_class.class_eval do
|
66
|
+
relation.class.send(:include, active_security_config.finder_methods)
|
67
|
+
extend(active_security_config.finder_methods)
|
68
|
+
end
|
69
|
+
|
70
|
+
association_relation_delegate_class = model_class.relation_delegate_class(::ActiveRecord::AssociationRelation)
|
71
|
+
association_relation_delegate_class.send(:include, model_class.active_security_config.finder_methods)
|
72
|
+
end
|
73
|
+
|
74
|
+
# Sets up behavior and configuration options for finders feature.
|
75
|
+
def included(model_class)
|
76
|
+
model_class.active_security_config.instance_eval do
|
77
|
+
self.class.send(:include, Configuration)
|
78
|
+
defaults[:default_finders] ||= :restricted
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sets up behavior and that depends on configuration
|
83
|
+
def after_config(model_class)
|
84
|
+
raise InvalidConfig, ":finders plugin must be used with default_finders set to one of :privileged, or :restricted" unless %i[restricted privileged].include?(model_class.active_security_config.default_finders)
|
85
|
+
|
86
|
+
model_class.active_security_config.use(model_class.active_security_config.default_finders)
|
87
|
+
model_class.class_eval do
|
88
|
+
if active_security_config.default_finders == :privileged
|
89
|
+
relation.class.send(:include, active_security_config.privileged_hooks)
|
90
|
+
send(:extend, active_security_config.privileged_hooks)
|
91
|
+
else
|
92
|
+
relation.class.send(:include, active_security_config.restricted_hooks)
|
93
|
+
send(:extend, active_security_config.restricted_hooks)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
association_relation_delegate_class = model_class.relation_delegate_class(::ActiveRecord::AssociationRelation)
|
97
|
+
if model_class.active_security_config.default_finders == :privileged
|
98
|
+
association_relation_delegate_class.send(:include, model_class.active_security_config.privileged_hooks)
|
99
|
+
else
|
100
|
+
model_class.active_security_config.default_finders
|
101
|
+
association_relation_delegate_class.send(:include, model_class.active_security_config.restricted_hooks)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# This module adds the `:default_finders` configuration option to
|
107
|
+
# {ActiveSecurity::Configuration ActiveSecurity::Configuration}.
|
108
|
+
module Configuration
|
109
|
+
# Gets the default_finders value.
|
110
|
+
#
|
111
|
+
# When setting this value, the argument should be a symbol, either
|
112
|
+
# :restricted or :privileged. Default is :restricted.
|
113
|
+
#
|
114
|
+
# @return Symbol The default_finders value
|
115
|
+
attr_accessor :default_finders
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
module Privileged
|
3
|
+
class << self
|
4
|
+
# Sets up behavior and configuration options for privileged feature.
|
5
|
+
def included(model_class)
|
6
|
+
model_class.active_security_config.instance_eval do
|
7
|
+
self.class.send(:include, Configuration)
|
8
|
+
defaults[:privileged_hooks] ||= ActiveSecurity::PrivilegedHooks
|
9
|
+
end
|
10
|
+
model_class.class_eval do
|
11
|
+
extend(PrivilegedScope)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# This module adds `:privileged_hooks` to
|
17
|
+
# {ActiveSecurity::Configuration ActiveSecurity::Configuration}.
|
18
|
+
module Configuration
|
19
|
+
attr_writer :privileged_hooks
|
20
|
+
|
21
|
+
def privileged_hooks
|
22
|
+
@privileged_hooks ||= defaults[:privileged_hooks]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module PrivilegedScope
|
27
|
+
# Returns a scope that is allowed to not require the secure scope, but will
|
28
|
+
# be logged.
|
29
|
+
# @see ActiveSecurity::FinderMethods
|
30
|
+
# @see ActiveSecurity::Privileged
|
31
|
+
def privileged
|
32
|
+
all.extending(
|
33
|
+
active_security_config.finder_methods,
|
34
|
+
active_security_config.privileged_hooks,
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
module PrivilegedHooks
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
def _active_security_not_scoped_handler
|
6
|
+
active_security_config.logger.warn("[Privileged] #{_active_security_name_for} does not have secure scope: #{respond_to?(:to_sql) ? to_sql : ""}")
|
7
|
+
end
|
8
|
+
|
9
|
+
def _active_security_unhandled_predicate(predicate)
|
10
|
+
active_security_config.logger.error("[Privileged] #{_active_security_name_for} predicate type #{predicate.class.name} is unhandled; See: https://www.rubydoc.info/github/rails/rails/Arel/Nodes")
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
module Restricted
|
3
|
+
class << self
|
4
|
+
# Sets up behavior and configuration options for restricted feature.
|
5
|
+
def included(model_class)
|
6
|
+
model_class.active_security_config.instance_eval do
|
7
|
+
self.class.send(:include, Configuration)
|
8
|
+
defaults[:restricted_hooks] ||= ActiveSecurity::RestrictedHooks
|
9
|
+
defaults[:on_restricted_no_scope] ||= :log_and_raise
|
10
|
+
defaults[:on_restricted_unhandled_predicate] ||= :log_and_raise
|
11
|
+
end
|
12
|
+
model_class.class_eval do
|
13
|
+
extend(RestrictedScope)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# This module adds `:restricted_hooks`, `:on_restricted_no_scope`
|
19
|
+
# and `:on_restricted_unhandled_predicate` configuration options to
|
20
|
+
# {ActiveSecurity::Configuration ActiveSecurity::Configuration}.
|
21
|
+
module Configuration
|
22
|
+
attr_writer :restricted_hooks
|
23
|
+
# Gets the on_restricted_no_scope value.
|
24
|
+
#
|
25
|
+
# When setting this value, the argument should either be a callable lambda/proc,
|
26
|
+
# or one of :log, :log_and_raise, :raise.
|
27
|
+
attr_writer :on_restricted_no_scope
|
28
|
+
|
29
|
+
# Gets the on_restricted_unhandled_predicate value.
|
30
|
+
#
|
31
|
+
# When setting this value, the argument should either be a callable lambda/proc,
|
32
|
+
# or one of :log, :log_and_raise, :raise.
|
33
|
+
attr_writer :on_restricted_unhandled_predicate
|
34
|
+
|
35
|
+
# @return Module The module to use for restricted_hooks
|
36
|
+
def restricted_hooks
|
37
|
+
@restricted_hooks ||= defaults[:restricted_hooks]
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return Symbol The on_restricted_no_scope value
|
41
|
+
def on_restricted_no_scope
|
42
|
+
@on_restricted_no_scope ||= defaults[:on_restricted_no_scope]
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return Symbol The on_restricted_unhandled_predicate value
|
46
|
+
def on_restricted_unhandled_predicate
|
47
|
+
@on_restricted_unhandled_predicate ||= defaults[:on_restricted_unhandled_predicate]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
module RestrictedScope
|
52
|
+
# Returns a scope that includes the active_security restricted hooks.
|
53
|
+
# @see ActiveSecurity::FinderMethods
|
54
|
+
# @see ActiveSecurity::RestrictedHooks
|
55
|
+
def restricted
|
56
|
+
# Guess what? This causes Rails to invoke `extend` on the scope, which has
|
57
|
+
# the well-known effect of blowing away Ruby's method cache. It would be
|
58
|
+
# possible to make this more performant by subclassing the model's
|
59
|
+
# relation class, extending that, and returning an instance of it in this
|
60
|
+
# method. However, using Rails' public API improves compatibility
|
61
|
+
# and maintainability. If you'd like to improve the performance, your
|
62
|
+
# efforts would be best directed at improving it at the root cause
|
63
|
+
# of the problem - in Rails - because it would benefit more people.
|
64
|
+
all.extending(
|
65
|
+
active_security_config.finder_methods,
|
66
|
+
active_security_config.restricted_hooks,
|
67
|
+
)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveSecurity
|
2
|
+
module RestrictedHooks
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
VALID_CONFIG_VALUES = %i[log log_and_raise raise]
|
6
|
+
|
7
|
+
def _active_security_not_scoped_handler
|
8
|
+
return active_security_config.on_restricted_no_scope.call(active_security_config) if active_security_config.on_restricted_no_scope.respond_to?(:call)
|
9
|
+
|
10
|
+
unless VALID_CONFIG_VALUES.include?(active_security_config.on_restricted_no_scope)
|
11
|
+
raise InvalidConfig, "on_restricted_no_scope must either be set to a (callable lambda/proc) or one of [:log, :log_and_raise, :raise]"
|
12
|
+
end
|
13
|
+
|
14
|
+
if /log/.match?(active_security_config.on_restricted_no_scope)
|
15
|
+
active_security_config.logger.error("#{_active_security_name_for} does not have secure scope: #{respond_to?(:to_sql) ? to_sql : ""}")
|
16
|
+
end
|
17
|
+
|
18
|
+
if /raise/.match?(active_security_config.on_restricted_no_scope)
|
19
|
+
raise RestrictedAccessError.new("prevented query without a secure scope #{_active_security_name_for}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def _active_security_unhandled_predicate(predicate)
|
24
|
+
return active_security_config.on_restricted_unhandled_predicate.call(active_security_config) if active_security_config.on_restricted_unhandled_predicate.respond_to?(:call)
|
25
|
+
|
26
|
+
unless VALID_CONFIG_VALUES.include?(active_security_config.on_restricted_unhandled_predicate)
|
27
|
+
raise InvalidConfig, "on_restricted_unhandled_predicate must either be set to a (callable lambda/proc) or one of [:log, :log_and_raise, :raise]"
|
28
|
+
end
|
29
|
+
|
30
|
+
if /log/.match?(active_security_config.on_restricted_unhandled_predicate)
|
31
|
+
active_security_config.logger.error("#{_active_security_name_for} predicate type #{predicate.class.name} is unhandled; See: https://www.rubydoc.info/github/rails/rails/Arel/Nodes")
|
32
|
+
end
|
33
|
+
|
34
|
+
if /raise/.match?(active_security_config.on_restricted_unhandled_predicate)
|
35
|
+
raise UnhandledArelPredicateError.new(
|
36
|
+
"#{_active_security_name_for} predicate type #{predicate.class.name} is unhandled; See: https://www.rubydoc.info/github/rails/rails/Arel/Nodes",
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|