cleanroom 1.0.0

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