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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +2 -0
- data/LICENSE +201 -0
- data/README.md +229 -0
- data/Rakefile +19 -0
- data/cleanroom.gemspec +37 -0
- data/lib/cleanroom.rb +188 -0
- data/lib/cleanroom/errors.rb +33 -0
- data/lib/cleanroom/rspec.rb +60 -0
- data/lib/cleanroom/version.rb +24 -0
- data/spec/functional/cleanroom_spec.rb +96 -0
- data/spec/spec_helper.rb +34 -0
- data/spec/unit/cleanroom_spec.rb +265 -0
- data/spec/unit/rspec_spec.rb +77 -0
- metadata +113 -0
data/Rakefile
ADDED
@@ -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)
|
data/cleanroom.gemspec
ADDED
@@ -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
|
data/lib/cleanroom.rb
ADDED
@@ -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
|