cleanroom 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.
@@ -0,0 +1,19 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ require 'rspec/core/rake_task'
4
+ [:unit, :functional].each do |type|
5
+ RSpec::Core::RakeTask.new(type) do |t|
6
+ t.pattern = "spec/#{type}/**/*_spec.rb"
7
+ t.rspec_opts = [].tap do |a|
8
+ a.push('--color')
9
+ a.push('--format progress')
10
+ end.join(' ')
11
+ end
12
+ end
13
+
14
+ namespace :travis do
15
+ desc 'Run tests on Travis'
16
+ task ci: %w(unit functional)
17
+ end
18
+
19
+ task default: %w(travis:ci)
@@ -0,0 +1,37 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'cleanroom'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'cleanroom'
8
+ spec.version = Cleanroom::VERSION
9
+ spec.author = 'Seth Vargo'
10
+ spec.email = 'sethvargo@gmail.com'
11
+ spec.summary = '(More) safely evaluate Ruby DSLs with cleanroom'
12
+ spec.description = <<-EOH.gsub(/^ {4}/, '').gsub(/\r?\n/, ' ').strip
13
+ Ruby is an excellent programming language for creating and managing custom
14
+ DSLs, but how can you securely evaluate a DSL while explicitly controlling
15
+ the methods exposed to the user? Our good friends instance_eval and
16
+ instance_exec are great, but they expose all methods - public, protected,
17
+ and private - to the user. Even worse, they expose the ability to
18
+ accidentally or intentionally alter the behavior of the system! The
19
+ cleanroom pattern is a safer, more convenient, Ruby-like approach for
20
+ limiting the information exposed by a DSL while giving users the ability to
21
+ write awesome code!
22
+ EOH
23
+ spec.homepage = 'https://github.com/sethvargo/cleanroom'
24
+ spec.license = 'Apache 2.0'
25
+
26
+ spec.required_ruby_version = '>= 1.9.3'
27
+
28
+ spec.files = `git ls-files -z`.split("\x0")
29
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
30
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_development_dependency 'rspec', '~> 3.0'
34
+
35
+ spec.add_development_dependency 'bundler'
36
+ spec.add_development_dependency 'rake'
37
+ end
@@ -0,0 +1,188 @@
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
+ module Cleanroom
21
+ #
22
+ # Callback for when this module is included.
23
+ #
24
+ # @param [Class] base
25
+ #
26
+ def self.included(base)
27
+ base.send(:extend, ClassMethods)
28
+ base.send(:include, InstanceMethods)
29
+ end
30
+
31
+ #
32
+ # Callback for when this module is included.
33
+ #
34
+ # @param [Class] base
35
+ #
36
+ def self.extended(base)
37
+ base.send(:extend, ClassMethods)
38
+ base.send(:include, InstanceMethods)
39
+ end
40
+
41
+ #
42
+ # Class methods
43
+ #
44
+ module ClassMethods
45
+ #
46
+ # Evaluate the file in the context of the cleanroom.
47
+ #
48
+ # @param [Class] instance
49
+ # the instance of the class to evaluate against
50
+ # @param [String] filepath
51
+ # the path of the file to evaluate
52
+ #
53
+ def evaluate_file(instance, filepath)
54
+ absolute_path = File.expand_path(filepath)
55
+ file_contents = IO.read(absolute_path)
56
+ evaluate(instance, file_contents, absolute_path, 1)
57
+ end
58
+
59
+ #
60
+ # Evaluate the string or block in the context of the cleanroom.
61
+ #
62
+ # @param [Class] instance
63
+ # the instance of the class to evaluate against
64
+ # @param [Array<String>] args
65
+ # the args to +instance_eval+
66
+ # @param [Proc] block
67
+ # the block to +instance_eval+
68
+ #
69
+ def evaluate(instance, *args, &block)
70
+ cleanroom.new(instance).instance_eval(*args, &block)
71
+ end
72
+
73
+ #
74
+ # Expose the given method to the DSL.
75
+ #
76
+ # @param [Symbol] name
77
+ #
78
+ def expose(name)
79
+ unless public_method_defined?(name)
80
+ raise NameError, "undefined method `#{name}' for class `#{self.name}'"
81
+ end
82
+
83
+ exposed_methods[name] = true
84
+ end
85
+
86
+ #
87
+ # The list of exposed methods.
88
+ #
89
+ # @return [Hash]
90
+ #
91
+ def exposed_methods
92
+ @exposed_methods ||= from_superclass(:exposed_methods, {}).dup
93
+ end
94
+
95
+ private
96
+
97
+ #
98
+ # The cleanroom instance for this class. This method is intentionally
99
+ # NOT cached!
100
+ #
101
+ # @return [Class]
102
+ #
103
+ def cleanroom
104
+ exposed = exposed_methods.keys
105
+ parent = self.name || 'Anonymous'
106
+
107
+ Class.new(Object) do
108
+ class << self
109
+ def class_eval
110
+ raise Cleanroom::InaccessibleError.new(:class_eval, self)
111
+ end
112
+
113
+ def instance_eval
114
+ raise Cleanroom::InaccessibleError.new(:instance_eval, self)
115
+ end
116
+ end
117
+
118
+ define_method(:initialize) do |instance|
119
+ define_singleton_method(:__instance__) do
120
+ unless caller[0].include?(__FILE__)
121
+ raise Cleanroom::InaccessibleError.new(:__instance__, self)
122
+ end
123
+
124
+ instance
125
+ end
126
+ end
127
+
128
+ exposed.each do |exposed_method|
129
+ define_method(exposed_method) do |*args, &block|
130
+ __instance__.public_send(exposed_method, *args, &block)
131
+ end
132
+ end
133
+
134
+ define_method(:class_eval) do
135
+ raise Cleanroom::InaccessibleError.new(:class_eval, self)
136
+ end
137
+
138
+ define_method(:inspect) do
139
+ "#<#{parent} (Cleanroom)>"
140
+ end
141
+ alias_method :to_s, :inspect
142
+ end
143
+ end
144
+
145
+ #
146
+ # Get the value from the superclass, if it responds, otherwise return
147
+ # +default+. Since class instance variables are **not** inherited upon
148
+ # subclassing, this is a required check to ensure subclasses inherit
149
+ # exposed DSL methods.
150
+ #
151
+ # @param [Symbol] m
152
+ # the name of the method to find
153
+ # @param [Object] default
154
+ # the default value to return if not found
155
+ #
156
+ def from_superclass(m, default = nil)
157
+ return default if superclass == Cleanroom
158
+ superclass.respond_to?(m) ? superclass.send(m) : default
159
+ end
160
+ end
161
+
162
+ #
163
+ # Instance Mehtods
164
+ #
165
+ module InstanceMethods
166
+ #
167
+ # Evaluate the file against the current instance.
168
+ #
169
+ # @param (see Cleanroom.evaluate_file)
170
+ # @return [self]
171
+ #
172
+ def evaluate_file(filepath)
173
+ self.class.evaluate_file(self, filepath)
174
+ self
175
+ end
176
+
177
+ #
178
+ # Evaluate the contents against the current instance.
179
+ #
180
+ # @param (see Cleanroom.evaluate_file)
181
+ # @return [self]
182
+ #
183
+ def evaluate(*args, &block)
184
+ self.class.evaluate(self, *args, &block)
185
+ self
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,33 @@
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
+ class Error < StandardError; end
19
+
20
+ class InaccessibleError < Error
21
+ def initialize(name, instance)
22
+ @name, @instance = name, instance
23
+ end
24
+
25
+ def to_s
26
+ <<-EOH.gsub(/\r?\n/, ' ')
27
+ Undefined local variable or method `#{@name}' for #{@instance}. It may have
28
+ been removed for the purposes of evaluating the DSL or for added security. If
29
+ you feel you have reached this message in error, please open an issue.
30
+ EOH
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,60 @@
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
+ unless defined?(RSpec)
20
+ require 'rspec'
21
+ end
22
+
23
+ #
24
+ # Assert a given method is exposed on a class.
25
+ #
26
+ # @example Checking against an instance
27
+ # expect(:method_1).to be_an_exposed_method_on(instance)
28
+ #
29
+ # @example Checking against a class
30
+ # expect(:method_1).to be_an_exposed_method_on(klass)
31
+ #
32
+ RSpec::Matchers.define :be_an_exposed_method_on do |object|
33
+ match do |name|
34
+ if object.is_a?(Class)
35
+ object.exposed_methods.key?(name.to_sym)
36
+ else
37
+ object.class.exposed_methods.key?(name.to_sym)
38
+ end
39
+ end
40
+ end
41
+
42
+ #
43
+ # Assert a given class or instance has an exposed method.
44
+ #
45
+ # @example Checking against an instance
46
+ # expect(instance).to have_exposed_method(:method_1)
47
+ #
48
+ # @example Checking against a class
49
+ # expect(klass).to have_exposed_method(:method_1)
50
+ #
51
+ RSpec::Matchers.define :have_exposed_method do |name|
52
+ match do |object|
53
+ if object.is_a?(Class)
54
+ object.exposed_methods.key?(name.to_sym)
55
+ else
56
+ object.class.exposed_methods.key?(name.to_sym)
57
+ end
58
+ end
59
+ end
60
+
@@ -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.0'
24
+ end
@@ -0,0 +1,96 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cleanroom do
4
+ let(:klass) do
5
+ Class.new do
6
+ NULL = Object.new.freeze unless defined?(NULL)
7
+
8
+ include Cleanroom
9
+
10
+ def method_1(val = NULL)
11
+ if val.equal?(NULL)
12
+ @method_1
13
+ else
14
+ @method_1 = val
15
+ end
16
+ end
17
+ expose :method_1
18
+
19
+ def method_2(val = NULL)
20
+ if val.equal?(NULL)
21
+ @method_2
22
+ else
23
+ @method_2 = val
24
+ end
25
+ end
26
+ expose :method_2
27
+
28
+ def method_3
29
+ @method_3 = true
30
+ end
31
+ end
32
+ end
33
+
34
+ let(:instance) { klass.new }
35
+
36
+ describe '#evaluate_file' do
37
+ let(:path) { tmp_path('file.rb') }
38
+
39
+ before do
40
+ File.open(path, 'w') do |f|
41
+ f.write <<-EOH.gsub(/^ {10}/, '')
42
+ method_1 'hello'
43
+ method_2 false
44
+ EOH
45
+ end
46
+ end
47
+
48
+ it 'evaluates the file' do
49
+ instance.evaluate_file(path)
50
+ expect(instance.method_1).to eq('hello')
51
+ expect(instance.method_2).to be(false)
52
+ end
53
+ end
54
+
55
+ describe '#evaluate' do
56
+ let(:contents) do
57
+ <<-EOH.gsub(/^ {8}/, '')
58
+ method_1 'hello'
59
+ method_2 false
60
+ EOH
61
+ end
62
+
63
+ it 'evaluates the file' do
64
+ instance.evaluate(contents)
65
+ expect(instance.method_1).to eq('hello')
66
+ expect(instance.method_2).to be(false)
67
+ end
68
+ end
69
+
70
+ describe 'security' do
71
+ it 'restricts access to __instance__' do
72
+ expect {
73
+ instance.evaluate("__instance__")
74
+ }.to raise_error(Cleanroom::InaccessibleError)
75
+ end
76
+
77
+ it 'restricts access to __instance__ using :send' do
78
+ expect {
79
+ instance.evaluate("send(:__instance__)")
80
+ }.to raise_error(Cleanroom::InaccessibleError)
81
+ end
82
+
83
+ it 'restricts access to defining new methods' do
84
+ expect {
85
+ instance.evaluate <<-EOH.gsub(/^ {12}/, '')
86
+ self.class.class_eval do
87
+ def new_method
88
+ __instance__.method_3
89
+ end
90
+ end
91
+ EOH
92
+ }.to raise_error(Cleanroom::InaccessibleError)
93
+ expect(instance.instance_variables).to_not include(:@method_3)
94
+ end
95
+ end
96
+ end