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