heimdallr 0.0.1 → 0.0.2
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/.gitignore +2 -0
- data/.yardopts +1 -0
- data/LICENSE +19 -0
- data/README.md +128 -0
- data/README.yard.md +128 -0
- data/Rakefile +20 -0
- data/heimdallr.gemspec +2 -3
- data/lib/heimdallr.rb +39 -2
- data/lib/heimdallr/evaluator.rb +139 -41
- data/lib/heimdallr/model.rb +49 -18
- data/lib/heimdallr/proxy/collection.rb +28 -0
- data/lib/heimdallr/proxy/record.rb +236 -0
- data/lib/heimdallr/resource.rb +220 -80
- data/lib/heimdallr/validator.rb +15 -0
- metadata +19 -13
- data/lib/heimdallr/proxy.rb +0 -61
- data/lib/heimdallr/version.rb +0 -3
data/.gitignore
CHANGED
data/.yardopts
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--no-private --protected -r README.yard.md --exclude README.md - LICENSE
|
data/LICENSE
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
7
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
8
|
+
so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
19
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
Heimdallr
|
2
|
+
=========
|
3
|
+
|
4
|
+
Heimdallr is a gem for managing security restrictions for ActiveRecord objects on field level; think
|
5
|
+
of it as a supercharged [CanCan](https://github.com/ryanb/cancan). Heimdallr favors whitelisting over blacklisting,
|
6
|
+
convention over configuration and is duck-type compatible with most of existing code.
|
7
|
+
|
8
|
+
``` ruby
|
9
|
+
# Define a typical set of models.
|
10
|
+
class User < ActiveRecord::Base
|
11
|
+
has_many :articles
|
12
|
+
end
|
13
|
+
|
14
|
+
class Article < ActiveRecord::Base
|
15
|
+
include Heimdallr::Model
|
16
|
+
|
17
|
+
belongs_to :owner, :class_name => 'User'
|
18
|
+
|
19
|
+
restrict do |user|
|
20
|
+
if user.admin? || user == self.owner
|
21
|
+
# Administrator or owner can do everything
|
22
|
+
can :fetch
|
23
|
+
can [:view, :create, :update, :destroy]
|
24
|
+
else
|
25
|
+
# Other users can view only non-classified articles...
|
26
|
+
can :fetch, -> { where('secrecy_level < ?', 5) }
|
27
|
+
|
28
|
+
# ... and see all fields except the actual security level...
|
29
|
+
can :view
|
30
|
+
cannot :view, [:secrecy_level]
|
31
|
+
|
32
|
+
# ... and can create them with certain restrictions.
|
33
|
+
can [:create, :update], {
|
34
|
+
owner: user,
|
35
|
+
secrecy_level: { inclusion: { in: 0..4 } }
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create some fictional data.
|
42
|
+
admin = User.create admin: true
|
43
|
+
johndoe = User.create admin: false
|
44
|
+
|
45
|
+
Article.create id: 1, owner: admin, content: "Nothing happens", secrecy_level: 0
|
46
|
+
Article.create id: 2, owner: admin, content: "This is a secret", secrecy_level: 10
|
47
|
+
Article.create id: 3, owner: johndoe, content: "Hello World"
|
48
|
+
|
49
|
+
# Get a restricted scope for the user.
|
50
|
+
secure = Article.restrict(johndoe)
|
51
|
+
|
52
|
+
# Use any ARel methods:
|
53
|
+
secure.pluck(:content)
|
54
|
+
# => ["Nothing happens", "Hello World"]
|
55
|
+
secure.find(1).secrecy_level
|
56
|
+
# => nil
|
57
|
+
|
58
|
+
# Everything should be permitted explicitly:
|
59
|
+
secure.first.delete
|
60
|
+
# ! Heimdallr::PermissionError is raised
|
61
|
+
|
62
|
+
# If only a single value is possible, it is inferred automatically:
|
63
|
+
secure.create! content: "My second article"
|
64
|
+
# => Article(id: 4, owner: johndoe, content: "My second article", security_level: 0)
|
65
|
+
|
66
|
+
# ... and cannot be changed:
|
67
|
+
secure.create! owner: admin, content: "I'm a haxx0r"
|
68
|
+
# ! ActiveRecord::RecordInvalid is raised
|
69
|
+
|
70
|
+
# You can use any valid ActiveRecord validators, too:
|
71
|
+
secure.create! content: "Top Secret", secrecy_level: 10
|
72
|
+
# ! ActiveRecord::RecordInvalid is raised
|
73
|
+
|
74
|
+
# John Doe would not see what he is not permitted to, ever:
|
75
|
+
# -- I know that you have this classified material! It's in folder #2.
|
76
|
+
secure.find 2
|
77
|
+
# ! ActiveRecord::RecordNotFound is raised
|
78
|
+
# -- No, it is not.
|
79
|
+
```
|
80
|
+
|
81
|
+
The DSL is described in documentation for [Heimdallr::Model](http://rubydoc.info/gems/heimdallr/0.0.2/Heimdallr/Model).
|
82
|
+
|
83
|
+
Note that Heimdallr is designed with three goals in mind, in the following order:
|
84
|
+
|
85
|
+
* Preventing malicious modifications
|
86
|
+
* Preventing information leaks
|
87
|
+
* Being convenient to use
|
88
|
+
|
89
|
+
Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
|
90
|
+
attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
|
91
|
+
going to change.
|
92
|
+
|
93
|
+
REST interface
|
94
|
+
--------------
|
95
|
+
|
96
|
+
Heimdallr also favors REST pattern; while its use is not mandated, a Heimdallr::Resource module is provided, which
|
97
|
+
implements all standard REST actions with the extension of allowing to pass multiple models at once, and also enables
|
98
|
+
one to introspect all writable fields with `new` and `edit` actions.
|
99
|
+
|
100
|
+
The interface is described in documentation for [Heimdallr::Resource](http://rubydoc.info/gems/heimdallr/0.0.2/Heimdallr/Resource).
|
101
|
+
|
102
|
+
Compatibility
|
103
|
+
-------------
|
104
|
+
|
105
|
+
Ruby 1.8 and ActiveRecord versions prior to 3.0 are not supported.
|
106
|
+
|
107
|
+
Licensing
|
108
|
+
---------
|
109
|
+
|
110
|
+
Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
|
111
|
+
|
112
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
113
|
+
this software and associated documentation files (the "Software"), to deal in
|
114
|
+
the Software without restriction, including without limitation the rights to
|
115
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
116
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
117
|
+
so, subject to the following conditions:
|
118
|
+
|
119
|
+
The above copyright notice and this permission notice shall be included in all
|
120
|
+
copies or substantial portions of the Software.
|
121
|
+
|
122
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
123
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
124
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
125
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
126
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
127
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
128
|
+
SOFTWARE.
|
data/README.yard.md
ADDED
@@ -0,0 +1,128 @@
|
|
1
|
+
Heimdallr
|
2
|
+
=========
|
3
|
+
|
4
|
+
Heimdallr is a gem for managing security restrictions for ActiveRecord objects on field level; think
|
5
|
+
of it as a supercharged [CanCan](https://github.com/ryanb/cancan). Heimdallr favors whitelisting over blacklisting,
|
6
|
+
convention over configuration and is duck-type compatible with most of existing code.
|
7
|
+
|
8
|
+
``` ruby
|
9
|
+
# Define a typical set of models.
|
10
|
+
class User < ActiveRecord::Base
|
11
|
+
has_many :articles
|
12
|
+
end
|
13
|
+
|
14
|
+
class Article < ActiveRecord::Base
|
15
|
+
include Heimdallr::Model
|
16
|
+
|
17
|
+
belongs_to :owner, :class_name => 'User'
|
18
|
+
|
19
|
+
restrict do |user|
|
20
|
+
if user.admin? || user == self.owner
|
21
|
+
# Administrator or owner can do everything
|
22
|
+
can :fetch
|
23
|
+
can [:view, :create, :update, :destroy]
|
24
|
+
else
|
25
|
+
# Other users can view only non-classified articles...
|
26
|
+
can :fetch, -> { where('secrecy_level < ?', 5) }
|
27
|
+
|
28
|
+
# ... and see all fields except the actual security level...
|
29
|
+
can :view
|
30
|
+
cannot :view, [:secrecy_level]
|
31
|
+
|
32
|
+
# ... and can create them with certain restrictions.
|
33
|
+
can [:create, :update], {
|
34
|
+
owner: user,
|
35
|
+
secrecy_level: { inclusion: { in: 0..4 } }
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create some fictional data.
|
42
|
+
admin = User.create admin: true
|
43
|
+
johndoe = User.create admin: false
|
44
|
+
|
45
|
+
Article.create id: 1, owner: admin, content: "Nothing happens", secrecy_level: 0
|
46
|
+
Article.create id: 2, owner: admin, content: "This is a secret", secrecy_level: 10
|
47
|
+
Article.create id: 3, owner: johndoe, content: "Hello World"
|
48
|
+
|
49
|
+
# Get a restricted scope for the user.
|
50
|
+
secure = Article.restrict(johndoe)
|
51
|
+
|
52
|
+
# Use any ARel methods:
|
53
|
+
secure.pluck(:content)
|
54
|
+
# => ["Nothing happens", "Hello World"]
|
55
|
+
secure.find(1).secrecy_level
|
56
|
+
# => nil
|
57
|
+
|
58
|
+
# Everything should be permitted explicitly:
|
59
|
+
secure.first.delete
|
60
|
+
# ! Heimdallr::PermissionError is raised
|
61
|
+
|
62
|
+
# If only a single value is possible, it is inferred automatically:
|
63
|
+
secure.create! content: "My second article"
|
64
|
+
# => Article(id: 4, owner: johndoe, content: "My second article", security_level: 0)
|
65
|
+
|
66
|
+
# ... and cannot be changed:
|
67
|
+
secure.create! owner: admin, content: "I'm a haxx0r"
|
68
|
+
# ! ActiveRecord::RecordInvalid is raised
|
69
|
+
|
70
|
+
# You can use any valid ActiveRecord validators, too:
|
71
|
+
secure.create! content: "Top Secret", secrecy_level: 10
|
72
|
+
# ! ActiveRecord::RecordInvalid is raised
|
73
|
+
|
74
|
+
# John Doe would not see what he is not permitted to, ever:
|
75
|
+
# -- I know that you have this classified material! It's in folder #2.
|
76
|
+
secure.find 2
|
77
|
+
# ! ActiveRecord::RecordNotFound is raised
|
78
|
+
# -- No, it is not.
|
79
|
+
```
|
80
|
+
|
81
|
+
The DSL is described in documentation for {Heimdallr::Model}.
|
82
|
+
|
83
|
+
Note that Heimdallr is designed with three goals in mind, in the following order:
|
84
|
+
|
85
|
+
* Preventing malicious modifications
|
86
|
+
* Preventing information leaks
|
87
|
+
* Being convenient to use
|
88
|
+
|
89
|
+
Due to the last one, not all methods will raise an exception on invalid access; some will silently drop the offending
|
90
|
+
attribute or simply return `nil`. This is clearly described in the documentation, done intentionally and isn't
|
91
|
+
going to change.
|
92
|
+
|
93
|
+
REST interface
|
94
|
+
--------------
|
95
|
+
|
96
|
+
Heimdallr also favors REST pattern; while its use is not mandated, a Heimdallr::Resource module is provided, which
|
97
|
+
implements all standard REST actions with the extension of allowing to pass multiple models at once, and also enables
|
98
|
+
one to introspect all writable fields with `new` and `edit` actions.
|
99
|
+
|
100
|
+
The interface is described in documentation for {Heimdallr::Resource}.
|
101
|
+
|
102
|
+
Compatibility
|
103
|
+
-------------
|
104
|
+
|
105
|
+
Ruby 1.8 and ActiveRecord versions prior to 3.0 are not supported.
|
106
|
+
|
107
|
+
Licensing
|
108
|
+
---------
|
109
|
+
|
110
|
+
Copyright (C) 2012 Peter Zotov <whitequark@whitequark.org>
|
111
|
+
|
112
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
113
|
+
this software and associated documentation files (the "Software"), to deal in
|
114
|
+
the Software without restriction, including without limitation the rights to
|
115
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
116
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
117
|
+
so, subject to the following conditions:
|
118
|
+
|
119
|
+
The above copyright notice and this permission notice shall be included in all
|
120
|
+
copies or substantial portions of the Software.
|
121
|
+
|
122
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
123
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
124
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
125
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
126
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
127
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
128
|
+
SOFTWARE.
|
data/Rakefile
CHANGED
@@ -1 +1,21 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
Dir.chdir File.dirname(__FILE__)
|
4
|
+
|
5
|
+
gemspec = Bundler.load_gemspec Dir["{,*}.gemspec"].first
|
6
|
+
|
7
|
+
task :release => :prepare_for_release
|
8
|
+
|
9
|
+
task :prepare_for_release do
|
10
|
+
readme = File.read('README.yard.md')
|
11
|
+
readme.gsub! /{([[:alnum:]:]+)}/i do |match|
|
12
|
+
%Q|[#{$1}](http://rubydoc.info/gems/#{gemspec.name}/#{gemspec.version}/#{$1.gsub '::', '/'})|
|
13
|
+
end
|
14
|
+
|
15
|
+
File.open('README.md', 'w') do |f|
|
16
|
+
f.write readme
|
17
|
+
end
|
18
|
+
|
19
|
+
%x|git add README.md|
|
20
|
+
#%x|git commit -m "Bump version to #{gemspec.version}."|
|
21
|
+
end
|
data/heimdallr.gemspec
CHANGED
@@ -1,16 +1,15 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
$:.push File.expand_path("../lib", __FILE__)
|
3
|
-
require "heimdallr/version"
|
4
3
|
|
5
4
|
Gem::Specification.new do |s|
|
6
5
|
s.name = "heimdallr"
|
7
|
-
s.version =
|
6
|
+
s.version = "0.0.2"
|
8
7
|
s.authors = ["Peter Zotov"]
|
9
8
|
s.email = ["whitequark@whitequark.org"]
|
10
9
|
s.homepage = "http://github.com/roundlake/heimdallr"
|
11
10
|
s.summary = %q{Heimdallr is an ActiveModel extension which provides object- and field-level access control.}
|
12
11
|
s.description = %q{Heimdallr aims to provide an easy to configure and efficient object- and field-level access
|
13
|
-
control solution, reusing proven patterns from gems like CanCan and allowing one to
|
12
|
+
control solution, reusing proven patterns from gems like CanCan and allowing one to manage permissions in a very
|
14
13
|
fine-grained manner.}
|
15
14
|
|
16
15
|
s.rubyforge_project = "heimdallr"
|
data/lib/heimdallr.rb
CHANGED
@@ -3,7 +3,44 @@ require "active_model"
|
|
3
3
|
|
4
4
|
require "heimdallr/version"
|
5
5
|
|
6
|
+
require "heimdallr/proxy/collection"
|
7
|
+
require "heimdallr/proxy/record"
|
8
|
+
require "heimdallr/validator"
|
6
9
|
require "heimdallr/evaluator"
|
7
|
-
require "heimdallr/proxy"
|
8
10
|
require "heimdallr/model"
|
9
|
-
require "heimdallr/resource"
|
11
|
+
require "heimdallr/resource"
|
12
|
+
|
13
|
+
# See {file:README.yard}.
|
14
|
+
module Heimdallr
|
15
|
+
class << self
|
16
|
+
# Allow implicit insecure association access. Consider this code:
|
17
|
+
#
|
18
|
+
# class User < ActiveRecord::Base
|
19
|
+
# include Heimdallr::Model
|
20
|
+
#
|
21
|
+
# has_many :articles
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# class Article < ActiveRecord::Base
|
25
|
+
# # No Heimdallr::Model!
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# If the +allow_insecure_associations+ setting is +false+ (the default),
|
29
|
+
# then +user.restrict(context).articles+ fetch would cause an
|
30
|
+
# {InsecureOperationError}. This may be undesirable in some environments;
|
31
|
+
# setting +allow_insecure_associations+ to +true+ will prevent the error
|
32
|
+
# from being raised.
|
33
|
+
#
|
34
|
+
# @return [Boolean]
|
35
|
+
attr_accessor :allow_insecure_associations
|
36
|
+
self.allow_insecure_associations = false
|
37
|
+
end
|
38
|
+
|
39
|
+
# {PermissionError} is raised when a security policy prevents
|
40
|
+
# a called operation from being executed.
|
41
|
+
class PermissionError < StandardError; end
|
42
|
+
|
43
|
+
# {InsecureOperationError} is raised when a potentially unsafe
|
44
|
+
# operation is about to be executed.
|
45
|
+
class InsecureOperationError < StandardError; end
|
46
|
+
end
|
data/lib/heimdallr/evaluator.rb
CHANGED
@@ -1,67 +1,149 @@
|
|
1
1
|
module Heimdallr
|
2
|
+
# Evaluator is a DSL for managing permissions on records with the field granularity.
|
3
|
+
# It works by evaluating a block of code within a given <em>security context</em>.
|
4
|
+
#
|
5
|
+
# The default resolution is to forbid everything--that is, Heimdallr security policy
|
6
|
+
# is whitelisting safe actions, not blacklisting unsafe ones. This is by design
|
7
|
+
# and is not going to change.
|
8
|
+
#
|
9
|
+
# The DSL consists of three functions: {#scope}, {#can} and {#cannot}.
|
2
10
|
class Evaluator
|
3
|
-
attr_reader :
|
11
|
+
attr_reader :allowed_fields, :fixtures, :validators
|
4
12
|
|
5
|
-
|
13
|
+
# Create a new Evaluator for the ActiveModel-descending class +model_class+,
|
14
|
+
# and use +block+ to infer restrictions for any security context passed.
|
15
|
+
def initialize(model_class, block)
|
6
16
|
@model_class, @block = model_class, block
|
7
17
|
|
8
|
-
@
|
9
|
-
@
|
18
|
+
@scopes = {}
|
19
|
+
@allowed_fields = {}
|
20
|
+
@validations = {}
|
21
|
+
@fixtures = {}
|
10
22
|
end
|
11
23
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
24
|
+
# @group DSL
|
25
|
+
|
26
|
+
# Define a scope. A special +:fetch+ scope is applied to any other scope
|
27
|
+
# automatically.
|
28
|
+
#
|
29
|
+
# @overload scope(name, block)
|
30
|
+
# This form accepts an explicit lambda.
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# scope :fetch, -> { where(:protected => false) }
|
34
|
+
#
|
35
|
+
# @overload scope(name)
|
36
|
+
# This form accepts an implicit lambda.
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# scope :fetch do
|
40
|
+
# if user.manager?
|
41
|
+
# scoped
|
42
|
+
# else
|
43
|
+
# where(:invisible => false)
|
44
|
+
# end
|
45
|
+
# end
|
46
|
+
def scope(name, explicit_block, &implicit_block)
|
47
|
+
@scopes[name] = explicit_block || implicit_block
|
48
|
+
end
|
21
49
|
|
22
|
-
|
50
|
+
# Define allowed operations for action(s).
|
51
|
+
#
|
52
|
+
# The +fields+ parameter accepts both Arrays and Hashes.
|
53
|
+
# * If an +Array+ is passed, then all fields present in the array are whitelised.
|
54
|
+
# * If a +Hash+ is passed, then all fields present as hash keys are whitelisted, and:
|
55
|
+
# 1. If a corresponding value is a +Hash+, it will be processed as a security
|
56
|
+
# validator. Security validators make records invalid when they are saved through
|
57
|
+
# a {Proxy::Record}.
|
58
|
+
# 2. If the corresponding value is any other object, it will be added as a security
|
59
|
+
# fixture. Fixtures are merged when objects are created through restricted scopes,
|
60
|
+
# and cause exceptions to be raised when a record is saved, even through the +#save+
|
61
|
+
# method.
|
62
|
+
#
|
63
|
+
# @example Array of fields
|
64
|
+
# can :view, [:title, :content]
|
65
|
+
#
|
66
|
+
# @example Fixtures
|
67
|
+
# can :create, { owner: current_user }
|
68
|
+
#
|
69
|
+
# @example Validations
|
70
|
+
# can [:create, :update], { priority: { inclusion: 1..10 } }
|
71
|
+
#
|
72
|
+
# @param [Symbol, Array<Symbol>] actions one or more action names
|
73
|
+
# @param [Hash<Hash, Object>] fields field restrictions
|
74
|
+
def can(actions, fields=@model_class.attribute_names)
|
75
|
+
Array(actions).each do |action|
|
76
|
+
case fields
|
77
|
+
when Hash # a list of validations
|
78
|
+
@allowed_fields[action] += fields.keys
|
79
|
+
@validations[action] += create_validators(fields)
|
80
|
+
@fixtures[action].merge extract_fixtures(fields)
|
81
|
+
|
82
|
+
else # an array or a field name
|
83
|
+
@allowed_fields[action] += Array(fields)
|
84
|
+
end
|
23
85
|
end
|
24
|
-
|
25
|
-
self
|
26
86
|
end
|
27
87
|
|
28
|
-
|
29
|
-
|
30
|
-
|
88
|
+
# Revoke a permission on fields.
|
89
|
+
#
|
90
|
+
# @todo Revoke validating restrictions.
|
91
|
+
# @param [Symbol, Array<Symbol>] actions one or more action names
|
92
|
+
# @param [Array<Symbol>] fields field list
|
93
|
+
def cannot(actions, fields)
|
94
|
+
Array(actions).each do |action|
|
95
|
+
@allowed_fields[action] -= fields
|
96
|
+
@fixtures.delete_at *fields
|
31
97
|
end
|
32
98
|
end
|
33
99
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
end
|
100
|
+
# @endgroup
|
101
|
+
|
102
|
+
# Request a scope.
|
103
|
+
#
|
104
|
+
# @param scope name of the scope
|
105
|
+
# @param basic_scope the scope to which scope +name+ will be applied. Defaults to +:fetch+.
|
106
|
+
#
|
107
|
+
# @return ActiveRecord scope
|
108
|
+
def request_scope(name=:fetch, basic_scope=request_scope(:fetch))
|
109
|
+
if name == :fetch || !@scopes.has_key?(name)
|
110
|
+
fetch_scope = @model_class.instance_exec(&@scopes[:fetch])
|
111
|
+
else
|
112
|
+
basic_scope.instance_exec(&@scopes[name])
|
48
113
|
end
|
49
114
|
end
|
50
115
|
|
51
|
-
|
52
|
-
|
116
|
+
# Compute the restrictions for a given +context+. Invokes a +block+ passed to the
|
117
|
+
# +initialize+ once.
|
118
|
+
def evaluate(context)
|
119
|
+
if context != @last_context
|
120
|
+
@scopes = {}
|
121
|
+
@allowed_fields = Hash.new { [] }
|
122
|
+
@validators = Hash.new { [] }
|
123
|
+
@fixtures = Hash.new { [] }
|
124
|
+
|
125
|
+
instance_exec context, &block
|
53
126
|
|
54
|
-
|
55
|
-
|
127
|
+
[@scopes, @allowed_fields, @validators, @fixtures].
|
128
|
+
map(&:freeze)
|
129
|
+
|
130
|
+
@last_context = context
|
56
131
|
end
|
132
|
+
|
133
|
+
self
|
57
134
|
end
|
58
135
|
|
59
136
|
protected
|
60
137
|
|
61
|
-
|
62
|
-
|
138
|
+
# Create validators for +fields+ in +ActiveModel::Validations+-like way.
|
139
|
+
#
|
140
|
+
# @return [Array<ActiveModel::Validator>]
|
141
|
+
def create_validators(fields)
|
142
|
+
validators = {}
|
63
143
|
|
64
144
|
fields.each do |attribute, validations|
|
145
|
+
next unless validations.is_a? Hash
|
146
|
+
|
65
147
|
validations.each do |key, options|
|
66
148
|
key = "#{key.to_s.camelize}Validator"
|
67
149
|
|
@@ -71,14 +153,30 @@ module Heimdallr
|
|
71
153
|
raise ArgumentError, "Unknown validator: '#{key}'"
|
72
154
|
end
|
73
155
|
|
74
|
-
validators
|
156
|
+
validators[attribute] = validator.new(_parse_validates_options(options).merge(:attributes => [ attribute ]))
|
75
157
|
end
|
76
158
|
end
|
77
159
|
|
78
160
|
validators
|
79
161
|
end
|
80
162
|
|
81
|
-
|
163
|
+
# Collects fixtures from the +fields+ definition.
|
164
|
+
def extract_fixtures(fields)
|
165
|
+
fixtures = {}
|
166
|
+
|
167
|
+
fields.each do |attribute, options|
|
168
|
+
next if options.is_a? Hash
|
169
|
+
|
170
|
+
fixtures[attribute] = options
|
171
|
+
end
|
172
|
+
|
173
|
+
fixtures
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
# Monkey-copied from ActiveRecord.
|
179
|
+
def _parse_validates_options(options)
|
82
180
|
case options
|
83
181
|
when TrueClass
|
84
182
|
{}
|