canned 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.md +39 -0
- data/Rakefile +23 -0
- data/lib/canned.rb +9 -0
- data/lib/canned/context/actor.rb +31 -0
- data/lib/canned/context/base.rb +40 -0
- data/lib/canned/context/default.rb +10 -0
- data/lib/canned/context/matchers/asks_for.rb +19 -0
- data/lib/canned/context/matchers/asks_with.rb +36 -0
- data/lib/canned/context/matchers/equality.rb +48 -0
- data/lib/canned/context/matchers/has.rb +23 -0
- data/lib/canned/context/matchers/helpers.rb +13 -0
- data/lib/canned/context/matchers/is.rb +23 -0
- data/lib/canned/context/matchers/load.rb +26 -0
- data/lib/canned/context/matchers/plus.rb +19 -0
- data/lib/canned/context/matchers/relation.rb +52 -0
- data/lib/canned/context/matchers/that.rb +23 -0
- data/lib/canned/context/matchers/the.rb +24 -0
- data/lib/canned/context/matchers/where.rb +36 -0
- data/lib/canned/context/multi.rb +8 -0
- data/lib/canned/context/resource.rb +11 -0
- data/lib/canned/context/value.rb +7 -0
- data/lib/canned/controller_ext.rb +216 -0
- data/lib/canned/definition.rb +79 -0
- data/lib/canned/errors.rb +6 -0
- data/lib/canned/profile.rb +62 -0
- data/lib/canned/profile_dsl.rb +130 -0
- data/lib/canned/stack.rb +63 -0
- data/lib/canned/version.rb +3 -0
- data/spec/canned/canned_spec.rb +116 -0
- data/spec/canned/controller_ext_spec.rb +95 -0
- data/spec/spec_helper.rb +35 -0
- metadata +113 -0
@@ -0,0 +1,130 @@
|
|
1
|
+
module Canned
|
2
|
+
|
3
|
+
## Holds all rules associated to a single user profile.
|
4
|
+
#
|
5
|
+
# This class describes the avaliable DSL when defining a new profile.
|
6
|
+
# TODO: example
|
7
|
+
#
|
8
|
+
class ProfileDsl
|
9
|
+
|
10
|
+
def initialize(_profile, _loaded_profiles)
|
11
|
+
@profile = _profile
|
12
|
+
@loaded_profiles = _loaded_profiles
|
13
|
+
end
|
14
|
+
|
15
|
+
## Sets the default context for this profile block.
|
16
|
+
def context(_proc=nil, &_block)
|
17
|
+
@profile.context = _proc || _block
|
18
|
+
end
|
19
|
+
|
20
|
+
## Adds an "allowance" rule
|
21
|
+
#
|
22
|
+
# Examples:
|
23
|
+
# allow 'index'
|
24
|
+
# allow 'index', upon(:user) { that(:is_admin) }
|
25
|
+
# allow('index') { upon(:user).that(:is_admin) }
|
26
|
+
# allow
|
27
|
+
# allow upon(:user) { that(:is_admin) }
|
28
|
+
# allow { upon(:user).that(:is_admin) }
|
29
|
+
#
|
30
|
+
# @param [String|Proc] _action The action to authorize, if no action is given then rule apply to any action.
|
31
|
+
# @param [Proc] _proc The test procedure, if not given, then action is always allowed.
|
32
|
+
#
|
33
|
+
def allow(_action=nil, _proc=nil)
|
34
|
+
|
35
|
+
if _action.is_a? Proc
|
36
|
+
_proc = _action
|
37
|
+
_action = nil
|
38
|
+
end
|
39
|
+
|
40
|
+
@profile.rules << { type: :allow, action: _action, proc: _proc }
|
41
|
+
end
|
42
|
+
|
43
|
+
## Adds a "forbidden" rule
|
44
|
+
#
|
45
|
+
# Works the same way as **allow** but if rule checks then user is forbidden to access
|
46
|
+
# the resource regardles of presenting another profile that passes.
|
47
|
+
#
|
48
|
+
def forbid(_action=nil, _proc=nil)
|
49
|
+
|
50
|
+
if _action.is_a? Proc
|
51
|
+
_proc = _action
|
52
|
+
_action = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
@profile.rules << { type: :forbid, action: _action, proc: _proc }
|
56
|
+
end
|
57
|
+
|
58
|
+
## Breaks from the current profile scope if condition is not match.
|
59
|
+
#
|
60
|
+
# When calling this function from within a scope, it will only break from scope.
|
61
|
+
#
|
62
|
+
# Example:
|
63
|
+
# # The following rules will be tested against every user
|
64
|
+
# allow 'index', upon(:user) { that(:is_registered) }
|
65
|
+
# allow 'index', upon(:user) { that(:is_alien) }
|
66
|
+
# allow 'index', upon(:user) { that(:is_chewbaka) }
|
67
|
+
#
|
68
|
+
# continue upon(:user) { that(:is_jedi) }
|
69
|
+
# # The following rules will only be tested against jedis
|
70
|
+
# allow 'index', upon(:user) { with(:force).greater_than(100) }
|
71
|
+
#
|
72
|
+
#
|
73
|
+
def continue(_proc)
|
74
|
+
@profile.rules << { type: :continue, proc: _proc }
|
75
|
+
end
|
76
|
+
|
77
|
+
## Embedds a _profile inside another one.
|
78
|
+
#
|
79
|
+
def expand(_profile)
|
80
|
+
profile = @loaded_profiles[_profile]
|
81
|
+
raise SetupError.new "Profile not found '#{_profile}'" if profile.nil?
|
82
|
+
@profile.rules << { type: :expand, profile: profile }
|
83
|
+
end
|
84
|
+
|
85
|
+
## Allows defining a set of rules with common options.
|
86
|
+
#
|
87
|
+
# Example:
|
88
|
+
# # The following group
|
89
|
+
# scope upon: :user
|
90
|
+
# # the following only breaks from current scope.
|
91
|
+
# continue upon { that(:is_jedi) }
|
92
|
+
# # the following will only forbid jedis that belong to death star (resource).
|
93
|
+
# forbid 'index', upon { belongs_to(:death_star) }
|
94
|
+
# allow 'index', upon(:user) { that(:has_pony_tail) }
|
95
|
+
# end
|
96
|
+
#
|
97
|
+
# # the following rule will be tested against every user.
|
98
|
+
# allow 'index', upon(:user) { that(:is_registered) }
|
99
|
+
#
|
100
|
+
#
|
101
|
+
def scope(&_block)
|
102
|
+
child = Profile.new
|
103
|
+
ProfileDsl.new(child, @loaded_profiles).instance_eval &_block
|
104
|
+
@profile.rules << { type: :scope, profile: child }
|
105
|
+
end
|
106
|
+
|
107
|
+
## SHORT HAND METHODS
|
108
|
+
|
109
|
+
## Same as calling upon { the() ... }
|
110
|
+
def upon(*_args, &_block)
|
111
|
+
if _args.count == 0
|
112
|
+
return _block
|
113
|
+
elsif _block
|
114
|
+
Proc.new { the(*_args).instance_eval &_block }
|
115
|
+
else
|
116
|
+
Proc.new { the(*_args) }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# TODO: Implement following
|
121
|
+
|
122
|
+
# def upon_one(_expr, &_block)
|
123
|
+
# Proc.new { upon_one(_expr, &_block) }
|
124
|
+
# end
|
125
|
+
|
126
|
+
# def upon_all(_expr, &_block)
|
127
|
+
# Proc.new { upon_all(_expr, &_block) }
|
128
|
+
# end
|
129
|
+
end
|
130
|
+
end
|
data/lib/canned/stack.rb
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
module Canned
|
2
|
+
|
3
|
+
## Implements an inmmutable stack where every operation generates a new stack
|
4
|
+
#
|
5
|
+
# Used by the test contexts to hold the subject stack.
|
6
|
+
#
|
7
|
+
class InmmutableStack
|
8
|
+
|
9
|
+
class NotFound < Exception; end
|
10
|
+
|
11
|
+
## Class used to hold stack entries
|
12
|
+
class Entry
|
13
|
+
|
14
|
+
attr_reader :tag # entry tag
|
15
|
+
attr_reader :name # entry name
|
16
|
+
attr_reader :obj # entry data
|
17
|
+
|
18
|
+
def initialize(_tag, _name, _obj)
|
19
|
+
@tag = _tag
|
20
|
+
@name = _name.to_s
|
21
|
+
@obj = _obj
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def initialize(_entry=nil, _tail=nil)
|
26
|
+
@stack = if _tail then _tail.entries else [] end
|
27
|
+
@stack << _entry if _entry
|
28
|
+
end
|
29
|
+
|
30
|
+
## Gets a copy of the internal stack state.
|
31
|
+
#
|
32
|
+
def entries; @stack.clone; end
|
33
|
+
|
34
|
+
## Returns true if stack is empty
|
35
|
+
def empty?; @stack.empty?; end
|
36
|
+
|
37
|
+
## Creates a new stack using the current stack as tail.
|
38
|
+
#
|
39
|
+
def push(_tag, _name, _value)
|
40
|
+
InmmutableStack.new Entry.new(_tag, _name, _value), self
|
41
|
+
end
|
42
|
+
|
43
|
+
## Retrieves the top value of the stack
|
44
|
+
#
|
45
|
+
def top(_tag=nil)
|
46
|
+
return @stack.last.obj if _tag.nil?
|
47
|
+
@stack.reverse_each do |item|
|
48
|
+
return item.obj if item.tag == _tag
|
49
|
+
end
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
## Resolves a stack value by it's name
|
54
|
+
#
|
55
|
+
def resolve(_name)
|
56
|
+
_name = _name.to_s
|
57
|
+
@stack.reverse_each do |item|
|
58
|
+
return item.obj if item.name == _name
|
59
|
+
end
|
60
|
+
raise NotFound
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Canned do
|
4
|
+
|
5
|
+
describe "TestProfiles.validate" do
|
6
|
+
|
7
|
+
context 'when using profile with a context' do
|
8
|
+
|
9
|
+
let(:definition) do
|
10
|
+
class TestProfiles
|
11
|
+
include Canned::Definition
|
12
|
+
|
13
|
+
profile :profile do
|
14
|
+
context { the(:user) }
|
15
|
+
allow 'rute1', upon { asks_with_same_id(:app_id) }
|
16
|
+
allow 'rute2', upon { asks_for(:test) }
|
17
|
+
forbid 'rute3', upon { not is(:is_admin) }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
TestProfiles
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'and a matching context' do
|
24
|
+
let(:context) do
|
25
|
+
dummy(
|
26
|
+
action_name: 'test',
|
27
|
+
actors: { user: dummy(app_id: 10, is_admin: false) },
|
28
|
+
resources: {},
|
29
|
+
params: { app_id: "10" }
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "is allowed if asks for same id" do
|
34
|
+
definition.validate(context, :profile, 'rute1').should == :allowed
|
35
|
+
end
|
36
|
+
|
37
|
+
it "is allowed if asks for same action" do
|
38
|
+
definition.validate(context, :profile, 'rute2').should == :allowed
|
39
|
+
end
|
40
|
+
|
41
|
+
it "is forbidden if 'is' expression returns true" do
|
42
|
+
definition.validate(context, :profile, 'rute3').should == :forbidden
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when using simple profile' do
|
48
|
+
|
49
|
+
let(:definition) do
|
50
|
+
class TestProfiles
|
51
|
+
include Canned::Definition
|
52
|
+
|
53
|
+
profile :profile do
|
54
|
+
allow 'action1', upon(:user) { asks_with_same_id(:app_id) }
|
55
|
+
allow 'action2', upon(:user) { belongs_to(:app, as: :app) }
|
56
|
+
allow 'action3', upon(:user) { asks_with_same(:app_id) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
TestProfiles
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'and an allowed context with actor and resource' do
|
63
|
+
let(:context) do
|
64
|
+
dummy(
|
65
|
+
action_name: 'test',
|
66
|
+
actors: { user: dummy(app_id: 10, is_admin: true) },
|
67
|
+
resources: { app: dummy(id: 10) },
|
68
|
+
params: { app_id: "10" }
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "is allowed if calls asks_for_same_id" do
|
73
|
+
definition.validate(context, :profile, 'action1').should == :allowed
|
74
|
+
end
|
75
|
+
|
76
|
+
it "is allowed if belongs_to resource" do
|
77
|
+
definition.validate(context, :profile, 'action2').should == :allowed
|
78
|
+
end
|
79
|
+
|
80
|
+
it "is not allowed if asks_for_same instead of asks_same_id" do
|
81
|
+
definition.validate(context, :profile, 'action3').should == :default
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
context 'and does not ask for same id' do
|
86
|
+
let(:context) do
|
87
|
+
dummy(
|
88
|
+
action_name: 'test',
|
89
|
+
actors: { user: dummy(app_id: 10, is_admin: true) },
|
90
|
+
resources: {},
|
91
|
+
params: { app_id: "11" }
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
it "is not allowed" do
|
96
|
+
definition.validate(context, :profile, 'action1').should == :default
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
context 'and does not belong to resource' do
|
101
|
+
let(:context) do
|
102
|
+
dummy(
|
103
|
+
action_name: 'test',
|
104
|
+
actors: { user: dummy(app_id: 10, is_admin: true) },
|
105
|
+
resources: { app: dummy(id: 11) },
|
106
|
+
params: {}
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
it "is not allowed" do
|
111
|
+
definition.validate(context, :profile, 'action2').should == :default
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Canned::ControllerExt do
|
4
|
+
|
5
|
+
let(:controller_class) do
|
6
|
+
controller_class = Class.new
|
7
|
+
controller_class.send(:include, Canned::ControllerExt)
|
8
|
+
controller_class
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:controller) do
|
12
|
+
controller = controller_class.new
|
13
|
+
controller.stub(:params) { { app_id: 10 } }
|
14
|
+
controller.stub(:controller_name) { 'controller' }
|
15
|
+
controller.stub(:action_name) { 'action' }
|
16
|
+
controller.stub(:good_user) { dummy(app_id: 10, is_admin: true) }
|
17
|
+
controller.stub(:bad_user) { dummy(app_id: 11) }
|
18
|
+
controller
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:definition) do
|
22
|
+
class TestProfiles
|
23
|
+
include Canned::Definition
|
24
|
+
|
25
|
+
profile :profile do
|
26
|
+
allow 'controller#action', upon(:user) { asks_with_id(:app_id).equal_to(own: :app_id) }
|
27
|
+
end
|
28
|
+
|
29
|
+
profile :profile2 do
|
30
|
+
allow 'controller#action', upon(:user) { belongs_to(:resource, as: :app) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
TestProfiles
|
34
|
+
end
|
35
|
+
|
36
|
+
describe ".is_restricted?" do
|
37
|
+
context 'when action is restricted' do
|
38
|
+
it { controller.is_restricted?.should be_true }
|
39
|
+
end
|
40
|
+
context 'when action is not restricted' do
|
41
|
+
before { controller_class.unrestricted :action }
|
42
|
+
it { controller.is_restricted?.should be_false }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe ".perform_resource_loading" do
|
47
|
+
|
48
|
+
context 'when registering resource for another action' do
|
49
|
+
let(:proxy) do
|
50
|
+
controller_class.register_resource(:resource, only: [:other]) { HashObj.new(id: 10) }
|
51
|
+
controller.perform_resource_loading
|
52
|
+
end
|
53
|
+
it { proxy.resources.has_key?(:resource).should be_false }
|
54
|
+
end
|
55
|
+
|
56
|
+
context 'when registering resource except for this action' do
|
57
|
+
let(:proxy) do
|
58
|
+
controller_class.register_resource(:resource, except: [:action]) { HashObj.new(id: 10) }
|
59
|
+
controller.perform_resource_loading
|
60
|
+
end
|
61
|
+
it { proxy.resources.has_key?(:resource).should be_false }
|
62
|
+
end
|
63
|
+
|
64
|
+
context 'when registering actor in a super class' do
|
65
|
+
let(:proxy) do
|
66
|
+
controller_class.register_actor(:actor) { HashObj.new(id: 10) }
|
67
|
+
sub_class = Class.new controller_class
|
68
|
+
other_controller = sub_class.new
|
69
|
+
other_controller.stub(:action_name) { 'action' }
|
70
|
+
other_controller.perform_resource_loading
|
71
|
+
end
|
72
|
+
it { proxy.actors.has_key?(:actor).should be_true }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe ".perform_access_authorization" do
|
77
|
+
|
78
|
+
context 'when registering and testing a valid actor' do
|
79
|
+
let(:result) do
|
80
|
+
controller_class.register_actor :good_user, as: :user
|
81
|
+
controller.perform_access_authorization(definition, [:profile])
|
82
|
+
end
|
83
|
+
it { result.should be_true }
|
84
|
+
end
|
85
|
+
|
86
|
+
context 'when registering and testing a valid actor and resource' do
|
87
|
+
let(:result) do
|
88
|
+
controller_class.register_actor :good_user, as: :user
|
89
|
+
controller_class.register_resource(:resource) { HashObj.new(id: 10) }
|
90
|
+
controller.perform_access_authorization(definition, [:profile2])
|
91
|
+
end
|
92
|
+
it { result.should be_true }
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'active_support/all'
|
10
|
+
require 'canned'
|
11
|
+
|
12
|
+
class HashObj
|
13
|
+
def initialize(_hash)
|
14
|
+
_hash.each do |k,v| self.class.send(:define_method, k) { v } end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
module Helpers
|
19
|
+
def dummy(_hash={})
|
20
|
+
HashObj.new _hash
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec.configure do |config|
|
25
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
26
|
+
config.run_all_when_everything_filtered = true
|
27
|
+
config.filter_run :focus
|
28
|
+
|
29
|
+
# Run specs in random order to surface order dependencies. If you find an
|
30
|
+
# order dependency and want to debug it, you can fix the order by providing
|
31
|
+
# the seed, which is printed after each run.
|
32
|
+
# --seed 1234
|
33
|
+
config.order = 'random'
|
34
|
+
config.include Helpers
|
35
|
+
end
|