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