canned 0.1.4
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.
- 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
|