cleanroom-next 1.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 05b7bbf84ca0dda05ebb3e5f4bff790af5e69b7f6f7081dd24549c759a84cf71
4
+ data.tar.gz: e4347462a89a0a8f1c11ce338da4f8dfab9ff65fbea6900c09c3678b8b332cf1
5
+ SHA512:
6
+ metadata.gz: 5fa0e7784212b46b7367f725e7113e91f68160721a961ddf131e08d3b5cd8db3944f6749b81d6ef1933a4a282b0be5c1bd840ac8f9c310d67ed4d4c871aae5c1
7
+ data.tar.gz: 1853e7e1fb508cba0a5d3270e7264ad0d192639f0e3c180edee211c786a24d42367fc596ebd96a7525ab6b25af5bc34926347d3d9916349a560e8bc447d8c133
@@ -0,0 +1,38 @@
1
+ #
2
+ # Copyright 2014 Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ module Cleanroom
18
+ # Base class for errors raised by Cleanroom
19
+ class Error < StandardError; end
20
+
21
+ # Error raised when trying to access a non accessible variable or method
22
+ class InaccessibleError < Error
23
+ def initialize(name, instance)
24
+ super()
25
+ @name = name
26
+ @instance = instance
27
+ end
28
+
29
+ # @return [String] The error message
30
+ def to_s
31
+ <<~ERROR_MESSAGE.gsub(/\r?\n/, ' ')
32
+ Undefined local variable or method `#{@name}' for #{@instance}. It may have
33
+ been removed for the purposes of evaluating the DSL or for added security. If
34
+ you feel you have reached this message in error, please open an issue.
35
+ ERROR_MESSAGE
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,57 @@
1
+ #
2
+ # Copyright 2014 Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require_relative '../cleanroom'
18
+
19
+ require 'rspec' unless defined?(RSpec)
20
+
21
+ #
22
+ # Assert a given method is exposed on a class.
23
+ #
24
+ # @example Checking against an instance
25
+ # expect(:method1).to be_an_exposed_method_on(instance)
26
+ #
27
+ # @example Checking against a class
28
+ # expect(:method1).to be_an_exposed_method_on(klass)
29
+ #
30
+ RSpec::Matchers.define :be_an_exposed_method_on do |object|
31
+ match do |name|
32
+ if object.is_a?(Class)
33
+ object.exposed_methods.key?(name.to_sym)
34
+ else
35
+ object.class.exposed_methods.key?(name.to_sym)
36
+ end
37
+ end
38
+ end
39
+
40
+ #
41
+ # Assert a given class or instance has an exposed method.
42
+ #
43
+ # @example Checking against an instance
44
+ # expect(instance).to have_exposed_method(:method1)
45
+ #
46
+ # @example Checking against a class
47
+ # expect(klass).to have_exposed_method(:method1)
48
+ #
49
+ RSpec::Matchers.define :have_exposed_method do |name|
50
+ match do |object|
51
+ if object.is_a?(Class)
52
+ object.exposed_methods.key?(name.to_sym)
53
+ else
54
+ object.class.exposed_methods.key?(name.to_sym)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,24 @@
1
+ #
2
+ # Copyright 2014 Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ module Cleanroom
18
+ #
19
+ # The version of the Cleanroom gem.
20
+ #
21
+ # @return [String]
22
+ #
23
+ VERSION = '1.0.1'
24
+ end
data/lib/cleanroom.rb ADDED
@@ -0,0 +1,236 @@
1
+ #
2
+ # Copyright 2014 Seth Vargo <sethvargo@gmail.com>
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require_relative 'cleanroom/errors'
18
+ require_relative 'cleanroom/version'
19
+
20
+ # The cleanroom pattern is a safer, more convenient, Ruby-like approach for
21
+ # limiting the information exposed by a DSL while giving users the ability to
22
+ # write awesome code!
23
+ module Cleanroom
24
+ #
25
+ # Callback for when this module is included.
26
+ #
27
+ # @param [Class] base
28
+ #
29
+ def self.included(base)
30
+ base.send(:extend, ClassMethods)
31
+ base.send(:include, InstanceMethods)
32
+ end
33
+
34
+ #
35
+ # Callback for when this module is included.
36
+ #
37
+ # @param [Class] base
38
+ #
39
+ def self.extended(base)
40
+ base.send(:extend, ClassMethods)
41
+ base.send(:include, InstanceMethods)
42
+ end
43
+
44
+ #
45
+ # Class methods
46
+ #
47
+ module ClassMethods
48
+ #
49
+ # Evaluate the file in the context of the cleanroom.
50
+ #
51
+ # @param [Class] instance
52
+ # the instance of the class to evaluate against
53
+ # @param [String] filepath
54
+ # the path of the file to evaluate
55
+ #
56
+ def evaluate_file(instance, filepath)
57
+ absolute_path = File.expand_path(filepath)
58
+ file_contents = File.read(absolute_path)
59
+ evaluate(instance, file_contents, absolute_path, 1)
60
+ end
61
+
62
+ #
63
+ # Evaluate the string or block in the context of the cleanroom.
64
+ #
65
+ # @param [Class] instance
66
+ # the instance of the class to evaluate against
67
+ def evaluate(instance, ...)
68
+ cleanroom.new(instance).instance_eval(...)
69
+ end
70
+
71
+ #
72
+ # Expose the given method to the DSL.
73
+ #
74
+ # @param [Symbol] name
75
+ #
76
+ def expose(name)
77
+ raise NameError, "undefined method `#{name}' for class `#{self.name}'" unless public_method_defined?(name)
78
+
79
+ exposed_methods_with_kwargs[name] = true if instance_method(name).parameters.any? { |(arg_type, _arg_name)| KWARGS_TYPES.include?(arg_type) }
80
+ exposed_methods[name] = true
81
+ end
82
+
83
+ #
84
+ # The list of exposed methods.
85
+ #
86
+ # @return [Hash]
87
+ #
88
+ def exposed_methods
89
+ @exposed_methods ||= from_superclass(:exposed_methods, {}).dup
90
+ end
91
+
92
+ private
93
+
94
+ # Define the types of argument types that point kwargs arguments.
95
+ # Useful to treat them differently as when defining a method with kwargs, Ruby will pass parameters having a to_hash method differently to such methods:
96
+ #
97
+ # See this example illustrating the difference in treatment with and without kwargs in the method definition:
98
+ # def without_kwargs(*args)
99
+ # p args
100
+ # end
101
+ # def with_kwargs(*args, **kwargs)
102
+ # p args
103
+ # p kwargs
104
+ # end
105
+ # s_without_to_hash = 'Without to_hash'
106
+ # s_with_to_hash = 'With to_hash'
107
+ # s_with_to_hash.define_singleton_method(:to_hash) { { string: self.to_s } }
108
+ # without_kwargs(s_without_to_hash)
109
+ # ["Without to_hash"]
110
+ # without_kwargs(s_with_to_hash)
111
+ # ["With to_hash"]
112
+ # with_kwargs(s_without_to_hash)
113
+ # ["Without to_hash"]
114
+ # {}
115
+ # with_kwargs(s_with_to_hash)
116
+ # []
117
+ # {:string=>"With to_hash"}
118
+ KWARGS_TYPES = %i[key keyreq]
119
+
120
+ #
121
+ # The list of exposed methods with kwargs.
122
+ #
123
+ # @return [Hash]
124
+ #
125
+ def exposed_methods_with_kwargs
126
+ @exposed_methods_with_kwargs ||= from_superclass(:exposed_methods_with_kwargs, {}).dup
127
+ end
128
+
129
+ #
130
+ # The cleanroom instance for this class. This method is intentionally
131
+ # NOT cached!
132
+ #
133
+ # @return [Class]
134
+ #
135
+ def cleanroom
136
+ exposed = exposed_methods.keys
137
+ exposed_with_kwargs = exposed_methods_with_kwargs.keys
138
+ parent = name || 'Anonymous'
139
+
140
+ Class.new(Object) do
141
+ class << self
142
+ def class_eval
143
+ raise Cleanroom::InaccessibleError.new(:class_eval, self)
144
+ end
145
+
146
+ def instance_eval
147
+ raise Cleanroom::InaccessibleError.new(:instance_eval, self)
148
+ end
149
+ end
150
+
151
+ define_method(:initialize) do |instance|
152
+ define_singleton_method(:__instance__) do
153
+ # Since Ruby 2.6, the first frame of `caller` for code evaluated via
154
+ # `instance_eval`/`eval` is labeled "(eval at <file>:<line>)", where
155
+ # the location is the very call site of `instance_eval` - here
156
+ # cleanroom.rb itself. That made the previous
157
+ # `caller[0].include?(__FILE__)` check wrongly grant DSL code access
158
+ # to the wrapped instance. Eval frames must be excluded: only real
159
+ # frames inside this file may call __instance__.
160
+ calling_frame = caller_locations(1, 1).first
161
+ unless calling_frame && !calling_frame.path.start_with?('(') && calling_frame.path.include?(__FILE__)
162
+ raise Cleanroom::InaccessibleError.new(:__instance__, self)
163
+ end
164
+
165
+ instance
166
+ end
167
+ end
168
+
169
+ (exposed - exposed_with_kwargs).each do |exposed_method|
170
+ define_method(exposed_method) do |*args, &block|
171
+ __instance__.public_send(exposed_method, *args, &block)
172
+ end
173
+ end
174
+
175
+ exposed_with_kwargs.each do |exposed_method|
176
+ define_method(exposed_method) do |*args, **kwargs, &block|
177
+ __instance__.public_send(exposed_method, *args, **kwargs, &block)
178
+ end
179
+ end
180
+
181
+ define_method(:class_eval) do
182
+ raise Cleanroom::InaccessibleError.new(:class_eval, self)
183
+ end
184
+
185
+ define_method(:inspect) do
186
+ "#<#{parent} (Cleanroom)>"
187
+ end
188
+ alias_method :to_s, :inspect
189
+ end
190
+ end
191
+
192
+ #
193
+ # Get the value from the superclass, if it responds, otherwise return
194
+ # +default+. Since class instance variables are **not** inherited upon
195
+ # subclassing, this is a required check to ensure subclasses inherit
196
+ # exposed DSL methods.
197
+ #
198
+ # @param [Symbol] method_name
199
+ # the name of the method to find
200
+ # @param [Object] default
201
+ # the default value to return if not found
202
+ #
203
+ def from_superclass(method_name, default = nil)
204
+ return default if superclass == Cleanroom
205
+
206
+ superclass.respond_to?(method_name) ? superclass.send(method_name) : default
207
+ end
208
+ end
209
+
210
+ #
211
+ # Instance Methods
212
+ #
213
+ module InstanceMethods
214
+ #
215
+ # Evaluate the file against the current instance.
216
+ #
217
+ # @param (see Cleanroom.evaluate_file)
218
+ # @return [self]
219
+ #
220
+ def evaluate_file(filepath)
221
+ self.class.evaluate_file(self, filepath)
222
+ self
223
+ end
224
+
225
+ #
226
+ # Evaluate the contents against the current instance.
227
+ #
228
+ # @param (see Cleanroom.evaluate_file)
229
+ # @return [self]
230
+ #
231
+ def evaluate(...)
232
+ self.class.evaluate(self, ...)
233
+ self
234
+ end
235
+ end
236
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cleanroom-next
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Seth Vargo
8
+ - Muriel Salvan
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: Ruby is an excellent programming language for creating and managing custom
14
+ DSLs, but how can you securely evaluate a DSL while explicitly controlling the methods
15
+ exposed to the user? Our good friends instance_eval and instance_exec are great,
16
+ but they expose all methods - public, protected, and private - to the user. Even
17
+ worse, they expose the ability to accidentally or intentionally alter the behavior
18
+ of the system! The cleanroom pattern is a safer, more convenient, Ruby-like approach
19
+ for limiting the information exposed by a DSL while giving users the ability to
20
+ write awesome code!
21
+ email:
22
+ - sethvargo@gmail.com
23
+ - muriel@x-aeon.com
24
+ executables: []
25
+ extensions: []
26
+ extra_rdoc_files: []
27
+ files:
28
+ - lib/cleanroom.rb
29
+ - lib/cleanroom/errors.rb
30
+ - lib/cleanroom/rspec.rb
31
+ - lib/cleanroom/version.rb
32
+ homepage: https://github.com/Muriel-Salvan/cleanroom
33
+ licenses:
34
+ - Apache-2.0
35
+ metadata:
36
+ rubygems_mfa_required: 'true'
37
+ rdoc_options: []
38
+ require_paths:
39
+ - lib
40
+ required_ruby_version: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: '3.0'
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ requirements: []
51
+ rubygems_version: 3.6.9
52
+ specification_version: 4
53
+ summary: "(More) safely evaluate Ruby DSLs with cleanroom"
54
+ test_files: []