Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Cleanroom Pattern

502828deee7e3b38ca1e527dded8a1a9?s=47 Seth Vargo
August 12, 2014

The Cleanroom Pattern

Ruby is an excellent programming language for creating and managing custom DSLs, but how can you securely evaluate a DSL while explicitly controlling the methods exposed to the user? Our good friends instance_eval and instance_exec are great, but they expose all methods - public, protected, and private - to the user. Even worse, they expose the ability to accidentally or intentionally alter the behavior of the system! The cleanroom pattern is a safer, more convenient, Ruby-like approach for limiting the information exposed by a DSL while giving users the ability to write awesome code!

The cleanroom pattern is a unique way for more safely evaluating Ruby DSLs without adding additional overhead.

502828deee7e3b38ca1e527dded8a1a9?s=128

Seth Vargo

August 12, 2014
Tweet

Transcript

  1. DSL & THE CLEANROOM PATTERN

  2. @sethvargo Release Engineer @ Chef

  3. @sethvargo Ruby Developer

  4. DOMAIN SPECIFIC LANGUAGE DSL

  5. None
  6. class Project def name(val = NULL) set_or_return(:name, val) end def

    description(val = NULL) set_or_return(:description, val) end end
  7. class set_or_return( set_or_return( end def name(val = NULL) set_or_return(:name, val)

    end
  8. class set_or_return( set_or_return( end def description(val = NULL) set_or_return(:description, val)

    end
  9. class instance_variable_get( instance_variable_set( end def set_or_return(key, val) if val.equal?(NULL) instance_variable_get(:"@#{key}")

    else instance_variable_set(:"@#{key}", val) end end
  10. class # @overload name(val) # Sets the name of this

    project # @param [String] val # @overload name # Returns this project's name # @return [String] end # @overload name(val) # Sets the name of this project # @param [String] val # @overload name # Returns this project's name # @return [String] def name(val = NULL)
  11. class end def name(val) @name = val end def name

    @name end
  12. [1] pry(main)>

  13. [1] pry(main)> project = Project.new

  14. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158>

  15. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

  16. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

    => nil
  17. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

    => nil [3] pry(main)> project.name("hamlet")
  18. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

    => nil [3] pry(main)> project.name("hamlet") => "hamlet"
  19. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

    => nil [3] pry(main)> project.name("hamlet") => "hamlet" [4] pry(main)> project.name
  20. [1] pry(main)> project = Project.new => #<Project:0x007f943ba2f158> [2] pry(main)> project.name

    => nil [3] pry(main)> project.name("hamlet") => "hamlet" [4] pry(main)> project.name => "hamlet"
  21. [1] pry(main)> project = Project.new [2] pry(main)> project.name [3] pry(main)>

    project.name("hamlet") [4] pry(main)> project.name
  22. project = Project.new project.name project.name("hamlet") project.name

  23. project.name("hamlet")

  24. project.instance_eval do name("hamlet") end

  25. name("hamlet")

  26. DOMAIN SPECIFIC LANGUAGE DSL

  27. class set_or_return( set_or_return( # ... end name "Hamlet" description "A

    classic"
  28. class set_or_return( set_or_return( # ... end name "Hamlet" description "A

    classic" name #=> "Hamlet"
  29. class set_or_return( set_or_return( # ... end name "Hamlet" description "A

    #{name}"
  30. class set_or_return( set_or_return( # ... end name "Hamlet" description "A

    Hamlet"
  31. BasicObject#instance_eval Evaluates a string containing Ruby source code within the

    context of the receiver.
  32. BasicObject#instance_eval ... the variable self is set to obj while

    the code is executing, giving the code access to obj’s instance variables
  33. BasicObject#instance_eval ... the variable while the code is executing, the

    code access to variables giving the code access to obj’s instance variables
  34. class Project end

  35. class Project def self.load(path) end end

  36. class Project def self.load(path) contents = IO.read(path) end end

  37. class Project def self.load(path) contents = IO.read(path) filename = File.basename(path)

    end end
  38. class Project def self.load(path) contents = IO.read(path) filename = File.basename(path)

    new.tap do |i| i.instance_eval(contents, filename, 1) end end end
  39. Project.load("/path/to/file")

  40. </> RUBY CLASS

  41. </> RUBY INSTANCE

  42. </> RUBY INSTANCE

  43. </> RUBY INSTA

  44. </> RUBY INSTA RUBY

  45. </> RUBY

  46. RUBY name "hamlet" description # ...

  47. RUBY name "hamlet" description # ... CLASS

  48. RUBY name "hamlet" description # ... INSTANCE

  49. RUBY name "hamlet" description # ... INSTANCE

  50. RUBY name "hamlet" description # ... INSTA

  51. RUBY name "hamlet" description # ... self.name #=> "hamlet"

  52. METHOD SCOPE BIND #1

  53. METHOD SCOPE BIND #1 -JOKE

  54. BECAUSE THERE IS NO SCOPE

  55. class Project protected :name private :description end

  56. None
  57. class Project def method # ... end end

  58. class Project def method # ... end end </> RUBY

  59. class Project def method # ... end end instance_eval </>

    RUBY
  60. project.name

  61. project.name NoMethodError: protected method `name' called

  62. project.name NoMethodError: protected method `name' called

  63. project.name NoMethodError: protected method `name' called project.instance_eval { name }

  64. project.name NoMethodError: protected method `name' called project.instance_eval { name }

    "hamlet"
  65. SCOPE CREEP BIND #2

  66. A PROBABILITY FOR COLLISION

  67. class Project def name(val = NULL) set_or_return(:name, val) end end

  68. class Project def name(val = NULL) set_or_return(:name, val) end private

    def sanitize(val) end end
  69. class Project def name(val = NULL) set_or_return(:name, val) end private

    def sanitize(val) return val if val.equal?(NULL) end end
  70. class Project def name(val = NULL) set_or_return(:name, val) end private

    def sanitize(val) return val if val.equal?(NULL) val.downcase.gsub(/\s+/, "-") end end
  71. class Project def name(val = NULL) set_or_return(:name, sanitize(val)) end private

    def sanitize(val) return val if val.equal?(NULL) val.downcase.gsub(/\s+/, "-") end end
  72. RUBY name "Some String" self.name

  73. RUBY name "Some String" self.name #=> "some-string"

  74. RUBY def sanitize(val) val.upcase end name "Some String" self.name

  75. RUBY def sanitize(val) val.upcase end name "Some String" self.name #=>

    "SOME STRING"
  76. USELESS VALIDATION BIND #3

  77. BasicObject#instance_eval ... the variable while the code is executing, the

    code access to variables giving the code access to obj’s instance variables
  78. VALIDATION CAN BE BYPASSED

  79. class Project def set_or_return(key, val) if val.equal?(NULL) instance_variable_get(:"@#{key}") else instance_variable_set(:"@#{key}",

    val) end end end
  80. class Project def set_or_return(key, val) if val.equal?(NULL) instance_variable_get(:"@#{key}") else raise

    Error unless val.is_a?(String) instance_variable_set(:"@#{key}", val) end end end
  81. RUBY name Object.new

  82. RUBY name Object.new Error!

  83. RUBY @name = Object.new self.name

  84. RUBY self.name #=> #<Object:0x07> @name = Object.new

  85. CLASS_EVAL BIND #4

  86. CAN PERMANENTLY CHANGE CLASS BEHAVIOR

  87. RUBY self .class .instance_eval do def new_method puts "hello" end

    end CLASS
  88. RUBY self .class .class_eval do def sanitize(*) nil end end

    self.name
  89. RUBY self .class .class_eval do def sanitize(*) nil end end

    self.name #=> nil
  90. FOR ALL FUTURE INSTANCES

  91. Project.load("/path/to/file")

  92. None
  93. Project.load("/insecure_file")

  94. INTRODUCING THE CLEANROOM

  95. NON-CLEANROOM

  96. </> RUBY NON-CLEANROOM

  97. </> RUBY CLASS NON-CLEANROOM

  98. INSTANCE NON-CLEANROOM </> RUBY

  99. INSTANCE NON-CLEANROOM </> RUBY

  100. INSTANCE NON-CLEANROOM </> RUBY NO FILTER

  101. INSTANCE NON-CLEANROOM </> RUBY NO FILTER NO GUARDS

  102. CLEANROOM

  103. </> RUBY CLEANROOM

  104. CLASS </> RUBY CLEANROOM

  105. CLASS </> RUBY CLEANROOM INSTANCE

  106. CLASS </> RUBY CLEANROOM INSTANCE EXPOSED METHODS

  107. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS

  108. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS (DYNAMIC)

  109. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS (DYNAMIC) instance_eval

  110. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS (DYNAMIC) instance_eval

    PROXY
  111. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS PROXY (DYNAMIC)

    instance_eval 1
  112. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS PROXY (DYNAMIC)

    instance_eval 1 2
  113. CLASS </> RUBY CLEANROOM INSTANCE INSTANCE EXPOSED METHODS PROXY (DYNAMIC)

    instance_eval 1 2 3
  114. USING THE CLEANROOM

  115. class Project def name(val = NULL) set_or_return(:name, val) end end

    NON-CLEANROOM
  116. class Project def name(val = NULL) set_or_return(:name, val) end end

    CLEANROOM
  117. class Project include Cleanroom def name(val = NULL) set_or_return(:name, val)

    end end CLEANROOM
  118. class Project include Cleanroom def name(val = NULL) set_or_return(:name, val)

    end expose :name end CLEANROOM
  119. CLEANROOM THAT'S IT!

  120. CLEANROOM class Project def self.load(path) contents = IO.read(path) filename =

    File.basename(path) new.tap do |i| i.instance_eval(contents, filename, 1) end end end
  121. CLEANROOM class contents = IO.read(path) filename = File.basename(path) new.tap i.instance_eval(contents,

    filename, 1) end
  122. CLEANROOM Project.load("/file")

  123. CLEANROOM Project.load("/file")

  124. CLEANROOM Project.evaluate_file("/file")

  125. sethvargo/cleanroom !

  126. @sethvargo Questions?