kindergarten 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.travis.yml +12 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +89 -0
- data/Rakefile +7 -0
- data/TODO.md +6 -0
- data/kindergarten.gemspec +25 -0
- data/lib/kindergarten.rb +17 -0
- data/lib/kindergarten/exceptions.rb +19 -0
- data/lib/kindergarten/governesses.rb +19 -0
- data/lib/kindergarten/governesses/easy_governess.rb +11 -0
- data/lib/kindergarten/governesses/head_governess.rb +144 -0
- data/lib/kindergarten/governesses/strict_governess.rb +48 -0
- data/lib/kindergarten/orm/active_record.rb +42 -0
- data/lib/kindergarten/orm/governess.rb +41 -0
- data/lib/kindergarten/perimeter.rb +129 -0
- data/lib/kindergarten/sandbox.rb +76 -0
- data/lib/kindergarten/version.rb +3 -0
- data/spec/kindergarten/governess_spec.rb +67 -0
- data/spec/kindergarten/perimeter_spec.rb +112 -0
- data/spec/kindergarten/sandbox_spec.rb +52 -0
- data/spec/kindergarten_spec.rb +12 -0
- data/spec/orm/active_record_spec.rb +125 -0
- data/spec/orm_helper.rb +15 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/support/01_rspec_helper.rb +9 -0
- data/spec/support/db/.gitkeep +0 -0
- data/spec/support/dining_perimeter.rb +14 -0
- data/spec/support/drinking_perimeter.rb +39 -0
- data/spec/support/log/.gitkeep +0 -0
- data/spec/support/puppet_perimeter.rb +19 -0
- data/spec/support/spec_perimeter.rb +35 -0
- metadata +146 -0
data/.gitignore
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
spec/support/db/test.db
|
19
|
+
spec/support/log/test.log
|
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color -f d
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Hartog C. de Mik
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Kindergarten
|
2
|
+
|
3
|
+
[![Build Status](https://secure.travis-ci.org/coffeeaddict/kindergarten.png)](http://travis-ci.org/coffeeaddict/kindergarten)
|
4
|
+
|
5
|
+
[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/coffeeaddict/kindergarten)
|
6
|
+
|
7
|
+
A way to achieve modularity and modular security with a sandbox on steroids.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem 'kindergarten'
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install kindergarten
|
22
|
+
|
23
|
+
## Usage
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# define a child
|
27
|
+
child = User.find(2)
|
28
|
+
|
29
|
+
# define a module (perimeter) for the child to play in
|
30
|
+
class MyPlayModule < Kindergarten::Perimeter
|
31
|
+
# use can-can rules to govern the perimeter
|
32
|
+
govern do |child|
|
33
|
+
can :watch, Television
|
34
|
+
cannot :watch, CableTV
|
35
|
+
|
36
|
+
can :eat, Candy do |candy|
|
37
|
+
child.quotum.allows(candy)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# define methods for the sandbox
|
42
|
+
sandbox :watch_tv, :eat
|
43
|
+
|
44
|
+
def watch_tv(tv)
|
45
|
+
guard(:watch, tv)
|
46
|
+
child.watch(tv)
|
47
|
+
|
48
|
+
sleep(:four)
|
49
|
+
end
|
50
|
+
|
51
|
+
def eat(candy)
|
52
|
+
guard(:eat, candy)
|
53
|
+
child.eat(candy)
|
54
|
+
end
|
55
|
+
|
56
|
+
def sleep(len) # not_accessible_from_outside
|
57
|
+
child.sleep(len)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# load the child and the module into a sandbox
|
62
|
+
sandbox = Kindergarten.sandbox(child)
|
63
|
+
sandbox.load_module(MyPlayPerimeter)
|
64
|
+
|
65
|
+
# you can now call the sandboxed methods on the sandbox
|
66
|
+
sandbox.watch_tv(CableTV.new) # fails with Kindergarten::AccessDenied
|
67
|
+
30.times do
|
68
|
+
sandbox.eat(Liquorice.new) # fails after a while
|
69
|
+
end
|
70
|
+
|
71
|
+
sandbox.sleep(:long) # fails with NoMethodError
|
72
|
+
|
73
|
+
sandbox.allowed?(:watch, Television)
|
74
|
+
# => true
|
75
|
+
```
|
76
|
+
|
77
|
+
You are not restricted to only one perimeter/module - that would be most
|
78
|
+
boring...
|
79
|
+
|
80
|
+
Infact, the above is the essence of things - but there is much much more fun
|
81
|
+
hidden inside the Kindergarten. More will follow on the Wiki
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/TODO.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/kindergarten/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Hartog C. de Mik"]
|
6
|
+
gem.email = ["hartog@organisedminds.com"]
|
7
|
+
gem.description = %q{A kindergarten with a perimeter, a governess and a sandbox}
|
8
|
+
gem.summary = %q{Provide a kindergarten for your code to play in}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
12
|
+
gem.files = `git ls-files`.split("\n")
|
13
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
14
|
+
gem.name = "kindergarten"
|
15
|
+
gem.require_paths = ["lib"]
|
16
|
+
gem.version = Kindergarten::VERSION
|
17
|
+
|
18
|
+
gem.add_dependency('cancan', ['~> 1.6.8'])
|
19
|
+
gem.add_dependency('activesupport', ['~> 3.2'])
|
20
|
+
|
21
|
+
gem.add_development_dependency('rake')
|
22
|
+
gem.add_development_dependency('rspec', ['~> 2.10'])
|
23
|
+
gem.add_development_dependency('activerecord', ['~> 3.2'])
|
24
|
+
gem.add_development_dependency('sqlite3')
|
25
|
+
end
|
data/lib/kindergarten.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'cancan'
|
2
|
+
require 'active_support/core_ext'
|
3
|
+
|
4
|
+
require "kindergarten/version"
|
5
|
+
require "kindergarten/sandbox"
|
6
|
+
require "kindergarten/exceptions"
|
7
|
+
require "kindergarten/governesses"
|
8
|
+
require "kindergarten/perimeter"
|
9
|
+
|
10
|
+
module Kindergarten
|
11
|
+
class << self
|
12
|
+
def sandbox(child)
|
13
|
+
Kindergarten::Sandbox.new(child)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Kindergarten
|
2
|
+
# Signals unallowed access
|
3
|
+
class AccessDenied < CanCan::AccessDenied
|
4
|
+
def initialize(action, target, opts)
|
5
|
+
message = opts.delete(:message)
|
6
|
+
if message.blank?
|
7
|
+
name = target.is_a?(Class) ? target.name : target.class.name
|
8
|
+
message = "You are not allowed to #{action} that #{name.downcase}"
|
9
|
+
end
|
10
|
+
|
11
|
+
super(message, action, target)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class Perimeter
|
16
|
+
# Signals bad sandbox method implementation
|
17
|
+
class Unguarded < SecurityError; end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Kindergarten
|
2
|
+
# Hash with only allowed keys
|
3
|
+
class ScrubbedHash < Hash; end
|
4
|
+
|
5
|
+
# Hash with only allowed keys and untainted values
|
6
|
+
class RinsedHash < Hash; end
|
7
|
+
|
8
|
+
module Governesses
|
9
|
+
class << self
|
10
|
+
attr_accessor :forbidden_keys
|
11
|
+
end
|
12
|
+
|
13
|
+
self.forbidden_keys = []
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require "kindergarten/governesses/head_governess"
|
18
|
+
require "kindergarten/governesses/strict_governess"
|
19
|
+
require "kindergarten/governesses/easy_governess"
|
@@ -0,0 +1,144 @@
|
|
1
|
+
module Kindergarten
|
2
|
+
# The Governess keeps an eye on the child in the sandbox and makes sure
|
3
|
+
# she plays nicely and within the bounds of legality
|
4
|
+
#
|
5
|
+
class HeadGoverness
|
6
|
+
include CanCan::Ability
|
7
|
+
|
8
|
+
def initialize(child)
|
9
|
+
@child = child
|
10
|
+
@unguarded = false
|
11
|
+
@rules = []
|
12
|
+
end
|
13
|
+
|
14
|
+
# The governess is empty when no rules have been defined
|
15
|
+
def empty?
|
16
|
+
@rules.empty?
|
17
|
+
end
|
18
|
+
|
19
|
+
# Perform a sandbox method within the care of the governess.
|
20
|
+
#
|
21
|
+
# The HeadGoverness does nothing with it.
|
22
|
+
#
|
23
|
+
# @param [Symbol] method The name of the method that will be executed (for
|
24
|
+
# logging, record-keeping, raising, etc.)
|
25
|
+
# @return The result of the block
|
26
|
+
#
|
27
|
+
def governed(method, &block)
|
28
|
+
raise "You must specify a block" unless block_given?
|
29
|
+
|
30
|
+
return yield
|
31
|
+
end
|
32
|
+
|
33
|
+
# Check to see if the child can do something, increments @guard_count
|
34
|
+
#
|
35
|
+
# @param action Action to take
|
36
|
+
# @param target On given target
|
37
|
+
# @param opts [Hash] options
|
38
|
+
# @option opts [String] :message The message on access denied
|
39
|
+
#
|
40
|
+
# @raise [Kindergarten::AccessDenied] when the kindergarten is guarded and
|
41
|
+
# the action is not allowed
|
42
|
+
#
|
43
|
+
# @return The given target to allow
|
44
|
+
# def project(id)
|
45
|
+
# project = Project.find(id)
|
46
|
+
# guard(:view, project)
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
def guard(action, target, opts={})
|
50
|
+
if guarded? && cannot?(action, target)
|
51
|
+
raise Kindergarten::AccessDenied.new(action, target, opts)
|
52
|
+
end
|
53
|
+
|
54
|
+
# to allow
|
55
|
+
# def project(id)
|
56
|
+
# project = Project.find(id)
|
57
|
+
# guard(:view, project)
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
return target
|
61
|
+
end
|
62
|
+
|
63
|
+
# When a block is given, set the Governess to unguarded during the
|
64
|
+
# execution of the block
|
65
|
+
#
|
66
|
+
def unguarded(&block)
|
67
|
+
if block_given?
|
68
|
+
before = @unguarded
|
69
|
+
|
70
|
+
@unguarded = true
|
71
|
+
yield
|
72
|
+
@unguarded = before
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def guarded?
|
77
|
+
!unguarded?
|
78
|
+
end
|
79
|
+
|
80
|
+
def unguarded?
|
81
|
+
!!@unguarded
|
82
|
+
end
|
83
|
+
|
84
|
+
# Scrub a hash of any key that is not specified
|
85
|
+
#
|
86
|
+
# @param [Hash] attributes An attributes-hash to scrub
|
87
|
+
# @param [Symbol] list A list of allowed attributes
|
88
|
+
#
|
89
|
+
# @return [ScrubbedHash] a hash with only allowed keys
|
90
|
+
def scrub(attributes, *list)
|
91
|
+
list.map!(&:to_sym)
|
92
|
+
|
93
|
+
forbidden = Kindergarten::Governesses.forbidden_keys
|
94
|
+
|
95
|
+
Kindergarten::ScrubbedHash[
|
96
|
+
attributes.symbolize_keys!.delete_if do |key,value|
|
97
|
+
forbidden.include?(key) || !list.include?(key)
|
98
|
+
end
|
99
|
+
]
|
100
|
+
end
|
101
|
+
|
102
|
+
# Scrub a hash of any key that is not specified
|
103
|
+
#
|
104
|
+
# @param [Hash] attributes An attributes-hash to scrub
|
105
|
+
# @param [Hash] untaint_opts Specify a Regexp for each key. The value from
|
106
|
+
# the attributes will be matched against the regexp and replaced with
|
107
|
+
# the first result.
|
108
|
+
#
|
109
|
+
# specify :pass instead of a Regexp to let the value pass without
|
110
|
+
# matching (usefull for non strings, etc.)
|
111
|
+
#
|
112
|
+
# @return [RinsedHash] a hash with only allowed keys and untainted values
|
113
|
+
#
|
114
|
+
# @example Untaint
|
115
|
+
# rinse(attributes, :name => /^([a-Z0-9]+)/, :description => /([\w\s-]+)/mg)
|
116
|
+
#
|
117
|
+
# @example Pass on a Date attribute
|
118
|
+
# rinse(attributes, :name => /([\w\s-]+)/, :date => :pass)
|
119
|
+
#
|
120
|
+
# @example Bad practice
|
121
|
+
# # beware of the dot-star!
|
122
|
+
# rinse(attributes, :name => /(.*)/)
|
123
|
+
#
|
124
|
+
def rinse(attributes, untaint_opts)
|
125
|
+
untaint_opts.symbolize_keys!
|
126
|
+
|
127
|
+
scrubbed = scrub(attributes, *untaint_opts.keys)
|
128
|
+
|
129
|
+
scrubbed.each do |key, value|
|
130
|
+
untaint = untaint_opts[key]
|
131
|
+
next if untaint == :pass
|
132
|
+
|
133
|
+
match = value.match(untaint)
|
134
|
+
if match.nil?
|
135
|
+
scrubbed.delete key
|
136
|
+
else
|
137
|
+
scrubbed[key] = match[1]
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
return Kindergarten::RinsedHash[scrubbed]
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Kindergarten
|
2
|
+
# A very strict governess, forces all the sandbox methods to use the guard
|
3
|
+
# methods.
|
4
|
+
#
|
5
|
+
# @note
|
6
|
+
# Does not specify how to rollback when the guard method was not called,
|
7
|
+
# You're on your own there...
|
8
|
+
#
|
9
|
+
class StrictGoverness < HeadGoverness
|
10
|
+
# Check how often the perimeter guarded something
|
11
|
+
attr_reader :guard_count
|
12
|
+
|
13
|
+
def initialize(child)
|
14
|
+
super
|
15
|
+
@guard_count = 0
|
16
|
+
end
|
17
|
+
|
18
|
+
# Force the use of guard inside sandbox methods
|
19
|
+
#
|
20
|
+
# @raise Kindergarten::Perimeter::Unguarded when the guard count did not
|
21
|
+
# increment during the block execution
|
22
|
+
#
|
23
|
+
def governed(method, &block)
|
24
|
+
before = self.guard_count
|
25
|
+
res = yield
|
26
|
+
|
27
|
+
if @unguarded != true && self.guard_count == before
|
28
|
+
raise Kindergarten::Perimeter::Unguarded.new(
|
29
|
+
"#{method} was executed without propper guarding"
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
return res
|
34
|
+
end
|
35
|
+
|
36
|
+
# guard something and increment the guard count
|
37
|
+
def guard(*args)
|
38
|
+
@guard_count += 1
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
# allow something unguarded and increment the guard count
|
43
|
+
def unguarded(&block)
|
44
|
+
@guard_count += 1
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|