stffn-declarative_authorization 0.2.1 → 0.2.3
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +2 -0
- data/Rakefile +8 -0
- data/app/controllers/authorization_rules_controller.rb +103 -0
- data/app/controllers/authorization_usages_controller.rb +19 -0
- data/app/helpers/authorization_rules_helper.rb +84 -0
- data/app/views/authorization_rules/graph.dot.erb +49 -0
- data/app/views/authorization_rules/graph.html.erb +39 -0
- data/app/views/authorization_rules/index.html.erb +15 -0
- data/app/views/authorization_usages/index.html.erb +45 -0
- data/config/routes.rb +6 -0
- data/lib/authorization.rb +514 -0
- data/lib/helper.rb +51 -0
- data/lib/in_controller.rb +311 -0
- data/lib/in_model.rb +130 -0
- data/lib/maintenance.rb +174 -0
- data/lib/obligation_scope.rb +281 -0
- data/lib/rails_legacy.rb +14 -0
- data/lib/reader.rb +391 -0
- data/test/authorization_test.rb +576 -0
- data/test/controller_test.rb +361 -0
- data/test/dsl_reader_test.rb +157 -0
- data/test/helper_test.rb +96 -0
- data/test/maintenance_test.rb +15 -0
- data/test/model_test.rb +794 -0
- data/test/schema.sql +32 -0
- data/test/test_helper.rb +99 -0
- metadata +26 -2
data/CHANGELOG
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
* Added handling of Authorization::AuthorizationInController::ClassMethods.filter_access_to parameters that are of the form [:show, :update] instead of just :show, :update. [jeremyf]
|
2
|
+
|
1
3
|
* Added a authorization rules browser. See README for more information [sb]
|
2
4
|
|
3
5
|
* Added Model.using_access_control? to check if a model has model security activated [sb]
|
data/Rakefile
CHANGED
@@ -33,3 +33,11 @@ desc "clone the garlic repo (for running ci tasks)"
|
|
33
33
|
task :get_garlic do
|
34
34
|
sh "git clone git://github.com/ianwhite/garlic.git garlic"
|
35
35
|
end
|
36
|
+
|
37
|
+
desc "Expand filelist in src gemspec"
|
38
|
+
task :build_gemspec do
|
39
|
+
gemspec_data = File.read("declarative_authorization.gemspec.src")
|
40
|
+
gemspec_data.gsub!(/\.files = (.*)/) {|m| ".files = #{eval($1).inspect}"}
|
41
|
+
File.open("declarative_authorization.gemspec", "w") {|f| f.write(gemspec_data)}
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,103 @@
|
|
1
|
+
if Authorization::activate_authorization_rules_browser?
|
2
|
+
|
3
|
+
begin
|
4
|
+
# for nice auth_rules output:
|
5
|
+
require "parse_tree"
|
6
|
+
require "parse_tree_extensions"
|
7
|
+
require "ruby2ruby"
|
8
|
+
rescue LoadError; end
|
9
|
+
|
10
|
+
class AuthorizationRulesController < ApplicationController
|
11
|
+
filter_access_to :all, :require => :read
|
12
|
+
def index
|
13
|
+
respond_to do |format|
|
14
|
+
format.html do
|
15
|
+
@auth_rules_script = File.read("#{RAILS_ROOT}/config/authorization_rules.rb")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def graph
|
21
|
+
if params[:format] == "svg"
|
22
|
+
render :text => dot_to_svg(auth_to_dot(graph_options)),
|
23
|
+
:content_type => "image/svg+xml"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def auth_to_dot (options = {})
|
29
|
+
options = {
|
30
|
+
:effective_role_privs => true,
|
31
|
+
:privilege_hierarchy => false,
|
32
|
+
:filter_roles => nil,
|
33
|
+
:filter_contexts => nil,
|
34
|
+
:highlight_privilege => nil
|
35
|
+
}.merge(options)
|
36
|
+
|
37
|
+
@highlight_privilege = options[:highlight_privilege]
|
38
|
+
@roles = authorization_engine.roles
|
39
|
+
@roles = @roles.select {|r| r == options[:filter_roles] } if options[:filter_roles]
|
40
|
+
@role_hierarchy = authorization_engine.role_hierarchy
|
41
|
+
@privilege_hierarchy = authorization_engine.privilege_hierarchy
|
42
|
+
|
43
|
+
@contexts = authorization_engine.auth_rules.
|
44
|
+
collect {|ar| ar.contexts.to_a}.flatten.uniq
|
45
|
+
@contexts = @contexts.select {|c| c == options[:filter_contexts] } if options[:filter_contexts]
|
46
|
+
@context_privs = {}
|
47
|
+
@role_privs = {}
|
48
|
+
authorization_engine.auth_rules.each do |auth_rule|
|
49
|
+
@role_privs[auth_rule.role] ||= []
|
50
|
+
auth_rule.contexts.
|
51
|
+
select {|c| options[:filter_contexts].nil? or c == options[:filter_contexts]}.
|
52
|
+
each do |context|
|
53
|
+
@context_privs[context] ||= []
|
54
|
+
@context_privs[context] += auth_rule.privileges.to_a
|
55
|
+
@context_privs[context].uniq!
|
56
|
+
@role_privs[auth_rule.role] += auth_rule.privileges.collect {|p| [context, p, auth_rule.attributes.empty?, auth_rule.to_long_s]}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
if options[:effective_role_privs]
|
61
|
+
@roles.each do |role|
|
62
|
+
@role_privs[role] ||= []
|
63
|
+
(@role_hierarchy[role] || []).each do |lower_role|
|
64
|
+
@role_privs[role].concat(@role_privs[lower_role]).uniq!
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
if options[:privilege_hierarchy]
|
70
|
+
@context_privs.each do |context, privs|
|
71
|
+
privs.each do |priv|
|
72
|
+
context_lower_privs = (@privilege_hierarchy[priv] || []).
|
73
|
+
select {|p,c| c.nil? or c == context}.
|
74
|
+
collect {|p,c| p}
|
75
|
+
privs.concat(context_lower_privs).uniq!
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
render_to_string :template => 'authorization_rules/graph.dot.erb', :layout => false
|
81
|
+
end
|
82
|
+
|
83
|
+
def dot_to_svg (dot_data)
|
84
|
+
gv = IO.popen("/usr/bin/dot -q -Tsvg", "w+")
|
85
|
+
gv.puts dot_data
|
86
|
+
gv.close_write
|
87
|
+
gv.read
|
88
|
+
rescue IOError, Errno::EPIPE => e
|
89
|
+
raise Exception, "Error in call to graphviz: #{e}"
|
90
|
+
end
|
91
|
+
|
92
|
+
def graph_options
|
93
|
+
{
|
94
|
+
:effective_role_privs => !params[:effective_role_privs].blank?,
|
95
|
+
:privilege_hierarchy => !params[:privilege_hierarchy].blank?,
|
96
|
+
:filter_roles => params[:filter_roles].blank? ? nil : params[:filter_roles].to_sym,
|
97
|
+
:filter_contexts => params[:filter_contexts].blank? ? nil : params[:filter_contexts].to_sym,
|
98
|
+
:highlight_privilege => params[:highlight_privilege].blank? ? nil : params[:highlight_privilege].to_sym
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
end # activate_authorization_rules_browser?
|
@@ -0,0 +1,19 @@
|
|
1
|
+
if Authorization::activate_authorization_rules_browser?
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), %w{.. .. lib maintenance})
|
4
|
+
|
5
|
+
class AuthorizationUsagesController < ApplicationController
|
6
|
+
helper :authorization_rules
|
7
|
+
filter_access_to :all, :require => :read
|
8
|
+
# TODO set context?
|
9
|
+
|
10
|
+
def index
|
11
|
+
respond_to do |format|
|
12
|
+
format.html do
|
13
|
+
@auth_usages_by_controller = Authorization::Maintenance::Usage.usages_by_controller
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
end # activate_authorization_rules_browser?
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module AuthorizationRulesHelper
|
2
|
+
def syntax_highlight (rules)
|
3
|
+
regexps = {
|
4
|
+
:constant => [/(:)(\w+)/],
|
5
|
+
:proc => ['role', 'authorization', 'privileges'],
|
6
|
+
:statement => ['has_permission_on', 'if_attribute', 'includes', 'privilege', 'to'],
|
7
|
+
:operator => ['is', 'contains'],
|
8
|
+
:special => ['user', 'true', 'false'],
|
9
|
+
:preproc => ['do', 'end', /()(=>)/, /()(\{)/, /()(\})/, /()(\[)/, /()(\])/],
|
10
|
+
:comment => [/()(#.*$)/]#,
|
11
|
+
#:privilege => [:read],
|
12
|
+
#:context => [:conferences]
|
13
|
+
}
|
14
|
+
regexps.each do |name, res|
|
15
|
+
res.each do |re|
|
16
|
+
rules.gsub!(
|
17
|
+
re.is_a?(String) ? Regexp.new("(^|[^:])\\b(#{Regexp.escape(re)})\\b") :
|
18
|
+
(re.is_a?(Symbol) ? Regexp.new("()(:#{Regexp.escape(re.to_s)})\\b") : re),
|
19
|
+
"\\1<span class=\"#{name}\">\\2</span>")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
rules
|
23
|
+
end
|
24
|
+
|
25
|
+
def link_to_graph (title, options = {})
|
26
|
+
type = options[:type] || ''
|
27
|
+
link_to_function title, "$$('object')[0].data = '#{url_for :action => 'index', :format => 'svg', :type => type}'"
|
28
|
+
end
|
29
|
+
|
30
|
+
def navigation
|
31
|
+
link_to("Rules", authorization_rules_path) << ' | ' <<
|
32
|
+
link_to("Graphical view", graph_authorization_rules_path) << ' | ' <<
|
33
|
+
link_to("Usages", authorization_usages_path) #<< ' | ' <<
|
34
|
+
# 'Edit | ' <<
|
35
|
+
# link_to("XACML export", :action => 'index', :format => 'xacml')
|
36
|
+
end
|
37
|
+
|
38
|
+
def role_color (role, fill = false)
|
39
|
+
fill_colors = %w{#ffdddd #ddffdd #ddddff #ffffdd #ffddff #ddffff}
|
40
|
+
colors = %w{#dd0000 #00dd00 #0000dd #dddd00 #dd00dd #00dddd}
|
41
|
+
@@role_colors ||= {}
|
42
|
+
@@role_colors[role] ||= begin
|
43
|
+
idx = @@role_colors.length % colors.length
|
44
|
+
[colors[idx], fill_colors[idx]]
|
45
|
+
end
|
46
|
+
@@role_colors[role][fill ? 1 : 0]
|
47
|
+
end
|
48
|
+
|
49
|
+
def role_fill_color (role)
|
50
|
+
role_color(role, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
def auth_usage_info_classes (auth_info)
|
54
|
+
classes = []
|
55
|
+
if auth_info[:controller_permissions]
|
56
|
+
if auth_info[:controller_permissions][0]
|
57
|
+
classes << "catch-all" if auth_info[:controller_permissions][0].actions.include?(:all)
|
58
|
+
classes << "default-privilege" unless auth_info[:controller_permissions][0].privilege
|
59
|
+
classes << "default-context" unless auth_info[:controller_permissions][0].context
|
60
|
+
classes << "no-attribute-check" unless auth_info[:controller_permissions][0].attribute_check
|
61
|
+
end
|
62
|
+
else
|
63
|
+
classes << "unprotected"
|
64
|
+
end
|
65
|
+
classes * " "
|
66
|
+
end
|
67
|
+
|
68
|
+
def auth_usage_info_title (auth_info)
|
69
|
+
titles = []
|
70
|
+
if auth_usage_info_classes(auth_info) =~ /unprotected/
|
71
|
+
titles << "No filter_access_to call protects this action"
|
72
|
+
end
|
73
|
+
if auth_usage_info_classes(auth_info) =~ /no-attribute-check/
|
74
|
+
titles << "Action is not protected with attribute check"
|
75
|
+
end
|
76
|
+
if auth_usage_info_classes(auth_info) =~ /default-privilege/
|
77
|
+
titles << "Privilege set automatically from action name by :all rule"
|
78
|
+
end
|
79
|
+
if auth_usage_info_classes(auth_info) =~ /default-context/
|
80
|
+
titles << "Context set automatically from controller name by filter_access_to call without :context option"
|
81
|
+
end
|
82
|
+
titles * ". "
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
|
2
|
+
digraph rules {
|
3
|
+
compound = true
|
4
|
+
edge [arrowhead=open]
|
5
|
+
node [shape=box,fontname="sans-serif",fontsize="16"]
|
6
|
+
fontname="sans-serif";fontsize="16"
|
7
|
+
ranksep = "0.3"
|
8
|
+
//concentrate = true
|
9
|
+
rankdir = TB
|
10
|
+
{
|
11
|
+
node [shape=ellipse,style=filled]
|
12
|
+
//rank = source
|
13
|
+
<% @roles.each do |role| %>
|
14
|
+
"<%= role.inspect %>" [fillcolor="<%= role_fill_color(role) %>"]
|
15
|
+
// ,URL="javascript:set_filter({roles: '<%= role %>'})"
|
16
|
+
<% end %>
|
17
|
+
<% @roles.each do |role| %>
|
18
|
+
<% (@role_hierarchy[role] || []).each do |lower_role| %>
|
19
|
+
"<%= role.inspect %>" -> "<%= lower_role.inspect %>" [constraint=false,arrowhead=empty]
|
20
|
+
<% end %>
|
21
|
+
<% end %>
|
22
|
+
}
|
23
|
+
|
24
|
+
<% @contexts.each do |context| %>
|
25
|
+
subgraph cluster_<%= context %> {
|
26
|
+
label = "<%= context.inspect %>"
|
27
|
+
style=filled; fillcolor="#eeeeee"
|
28
|
+
node[fillcolor=white,style=filled]
|
29
|
+
<% (@context_privs[context] || []).each do |priv| %>
|
30
|
+
<%= priv %>_<%= context %> [label="<%= priv.inspect %>"<%= ',fontcolor="#ff0000"' if @highlight_privilege == priv %>]
|
31
|
+
<% end %>
|
32
|
+
<% (@context_privs[context] || []).each do |priv| %>
|
33
|
+
<% (@privilege_hierarchy[priv] || []).
|
34
|
+
select {|p,c| (c.nil? or c == context) and @context_privs[context].include?(p)}.
|
35
|
+
each do |lower_priv, c| %>
|
36
|
+
<%= priv %>_<%= context %> -> <%= lower_priv %>_<%= context %> [arrowhead=empty]
|
37
|
+
<% end %>
|
38
|
+
<% end %>
|
39
|
+
//read_conferences -> update_conferences [style=invis]
|
40
|
+
//create_conferences -> delete_conferences [style=invis]
|
41
|
+
}
|
42
|
+
<% end %>
|
43
|
+
|
44
|
+
<% @roles.each do |role| %>
|
45
|
+
<% (@role_privs[role] || []).each do |context, privilege, unconditionally, attribute_string| %>
|
46
|
+
"<%= role.inspect %>" -> <%= privilege %>_<%= context %> [color="<%= role_color(role) %>", minlen=3<%= ", arrowhead=opendot, URL=\"javascript:\", edgetooltip=\"#{attribute_string.gsub('"','')}\"" unless unconditionally %>]
|
47
|
+
<% end %>
|
48
|
+
<% end %>
|
49
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
<h1>Authorization Rules Graph</h1>
|
2
|
+
<p>Currently active rules in this application.</p>
|
3
|
+
<p><%= navigation %></p>
|
4
|
+
|
5
|
+
<% javascript_tag do %>
|
6
|
+
function update_graph (form) {
|
7
|
+
base_url = "<%= url_for :format => 'svg' %>";
|
8
|
+
$('graph').data = base_url + '?' + form.serialize();
|
9
|
+
}
|
10
|
+
|
11
|
+
function set_filter (filter) {
|
12
|
+
for (f in filter) {
|
13
|
+
var select = $("filter_" + f);
|
14
|
+
if (select) {
|
15
|
+
var opt = select.down("option[value='"+ filter[f] + "']");
|
16
|
+
if (opt) {
|
17
|
+
opt.selected = true;
|
18
|
+
update_graph(select.form);
|
19
|
+
}
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
23
|
+
<% end %>
|
24
|
+
<p>
|
25
|
+
<% form_tag do %>
|
26
|
+
<%#= link_to_graph "Rules" %>
|
27
|
+
<%#= link_to_graph "Privilege hierarchy", :type => 'priv_hierarchy' %>
|
28
|
+
|
29
|
+
<%= select_tag "filter_roles", options_for_select([["All roles",'']] + controller.authorization_engine.roles), :onchange => 'update_graph(this.form)' %>
|
30
|
+
<%= select_tag "filter_contexts", options_for_select([["All contexts",'']] + controller.authorization_engine.auth_rules.collect {|ar| ar.contexts.to_a}.flatten.uniq), :onchange => 'update_graph(this.form)' %>
|
31
|
+
<%= check_box_tag "effective_role_privs", "1", false, :onclick => 'update_graph(this.form)' %> <%= label_tag "effective_role_privs", "Effective privileges" %>
|
32
|
+
<%= check_box_tag "privilege_hierarchy", "1", false, :onclick => 'update_graph(this.form)' %> <%= label_tag "privilege_hierarchy", "Show full privilege hierarchy" %>
|
33
|
+
<% end %>
|
34
|
+
</p>
|
35
|
+
<div style="margin: 1em;border:1px solid #ccc;max-width:95%">
|
36
|
+
<object id="graph" data="<%= url_for :format => 'svg' %>" type="image/svg+xml" style="max-width:100%"/>
|
37
|
+
</div>
|
38
|
+
<%= button_to_function "Zoom in", '$("graph").style.maxWidth = "";$(this).toggle();$(this).next().toggle()' %>
|
39
|
+
<%= button_to_function "Zoom out", '$("graph").style.maxWidth = "100%";$(this).toggle();$(this).previous().toggle()', :style => 'display:none' %>
|
@@ -0,0 +1,15 @@
|
|
1
|
+
<h1>Authorization Rules</h1>
|
2
|
+
<p>Currently active rules in this application.</p>
|
3
|
+
<p><%= navigation %></p>
|
4
|
+
<style type="text/css">
|
5
|
+
pre .constant {color: #a00;}
|
6
|
+
pre .special {color: red;}
|
7
|
+
pre .operator {color: red;}
|
8
|
+
pre .statement {color: #00a;}
|
9
|
+
pre .proc {color: #0a0;}
|
10
|
+
pre .privilege, pre .context {font-weight: bold}
|
11
|
+
pre .preproc, pre .comment, pre .comment span {color: grey !important;}
|
12
|
+
</style>
|
13
|
+
<pre>
|
14
|
+
<%= syntax_highlight(h(@auth_rules_script)) %>
|
15
|
+
</pre>
|
@@ -0,0 +1,45 @@
|
|
1
|
+
<h1>Authorization Usage</h1>
|
2
|
+
<div style="margin: 1em;border:1px solid #ccc;max-width:50%;position:fixed;right:0;display:none">
|
3
|
+
<object id="graph" data="<%= url_for :format => 'svg' %>" type="image/svg+xml" style="max-width:100%"/>
|
4
|
+
</div>
|
5
|
+
<p>Filter rules in actions by controller:</p>
|
6
|
+
<p><%= navigation %></p>
|
7
|
+
<style type="text/css">
|
8
|
+
.auth-usages th { text-align: left; padding-top: 1em }
|
9
|
+
.auth-usages td { padding-right: 1em }
|
10
|
+
.auth-usages tr.action { cursor: pointer }
|
11
|
+
.auth-usages tr.unprotected { background: #FFA399 }
|
12
|
+
.auth-usages tr.no-attribute-check { background: #FFE599 }
|
13
|
+
/*.auth-usages tr.catch-all td.privilege,*/
|
14
|
+
.auth-usages tr.default-privilege td.privilege,
|
15
|
+
.auth-usages tr.default-context td.context { color: #888888 }
|
16
|
+
</style>
|
17
|
+
<% javascript_tag do %>
|
18
|
+
function show_graph (privilege, context) {
|
19
|
+
base_url = "<%= graph_authorization_rules_path('svg') %>";
|
20
|
+
$('graph').data = base_url + '?privilege_hierarchy=1&highlight_privilege=' +
|
21
|
+
privilege + '&filter_contexts=' + context;
|
22
|
+
$('graph').up().show();
|
23
|
+
}
|
24
|
+
<% end %>
|
25
|
+
<table class="auth-usages">
|
26
|
+
<% @auth_usages_by_controller.keys.sort {|c1, c2| c1.name <=> c2.name}.each do |controller| %>
|
27
|
+
<% default_context = controller.controller_name.to_sym rescue nil %>
|
28
|
+
<tr>
|
29
|
+
<th colspan="3"><%= h controller.controller_name %></th>
|
30
|
+
</tr>
|
31
|
+
<% @auth_usages_by_controller[controller].keys.sort {|c1, c2| c1.to_s <=> c2.to_s}.each do |action| %>
|
32
|
+
<% auth_info = @auth_usages_by_controller[controller][action] %>
|
33
|
+
<% first_permission = auth_info[:controller_permissions] && auth_info[:controller_permissions][0] %>
|
34
|
+
<tr class="action <%= auth_usage_info_classes(auth_info) %>" title="<%= auth_usage_info_title(auth_info) %>" onclick="show_graph('<%= auth_info[:privilege] || action %>','<%= auth_info[:context] || default_context %>')">
|
35
|
+
<td><%= h action %></td>
|
36
|
+
<% if first_permission %>
|
37
|
+
<td class="privilege"><%= h auth_info[:privilege] || action %></td>
|
38
|
+
<td class="context"><%= h auth_info[:context] || default_context %></td>
|
39
|
+
<% else %>
|
40
|
+
<td></td><td></td>
|
41
|
+
<% end %>
|
42
|
+
</tr>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
</table>
|
data/config/routes.rb
ADDED
@@ -0,0 +1,514 @@
|
|
1
|
+
# Authorization
|
2
|
+
require File.dirname(__FILE__) + '/reader.rb'
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
|
6
|
+
module Authorization
|
7
|
+
# An exception raised if anything goes wrong in the Authorization realm
|
8
|
+
class AuthorizationError < StandardError ; end
|
9
|
+
# NotAuthorized is raised if the current user is not allowed to perform
|
10
|
+
# the given operation possibly on a specific object.
|
11
|
+
class NotAuthorized < AuthorizationError ; end
|
12
|
+
# AttributeAuthorizationError is more specific than NotAuthorized, signalling
|
13
|
+
# that the access was denied on the grounds of attribute conditions.
|
14
|
+
class AttributeAuthorizationError < NotAuthorized ; end
|
15
|
+
# AuthorizationUsageError is used whenever a situation is encountered
|
16
|
+
# in which the application misused the plugin. That is, if, e.g.,
|
17
|
+
# authorization rules may not be evaluated.
|
18
|
+
class AuthorizationUsageError < AuthorizationError ; end
|
19
|
+
# NilAttributeValueError is raised by Attribute#validate? when it hits a nil attribute value.
|
20
|
+
# The exception is raised to ensure that the entire rule is invalidated.
|
21
|
+
class NilAttributeValueError < AuthorizationError; end
|
22
|
+
|
23
|
+
AUTH_DSL_FILE = "#{RAILS_ROOT}/config/authorization_rules.rb"
|
24
|
+
|
25
|
+
# Controller-independent method for retrieving the current user.
|
26
|
+
# Needed for model security where the current controller is not available.
|
27
|
+
def self.current_user
|
28
|
+
Thread.current["current_user"] || GuestUser.new
|
29
|
+
end
|
30
|
+
|
31
|
+
# Controller-independent method for setting the current user.
|
32
|
+
def self.current_user=(user)
|
33
|
+
Thread.current["current_user"] = user
|
34
|
+
end
|
35
|
+
|
36
|
+
@@ignore_access_control = false
|
37
|
+
# For use in test cases only
|
38
|
+
def self.ignore_access_control (state = nil) # :nodoc:
|
39
|
+
false
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.activate_authorization_rules_browser? # :nodoc:
|
43
|
+
::RAILS_ENV == 'development'
|
44
|
+
end
|
45
|
+
|
46
|
+
# Authorization::Engine implements the reference monitor. It may be used
|
47
|
+
# for querying the permission and retrieving obligations under which
|
48
|
+
# a certain privilege is granted for the current user.
|
49
|
+
#
|
50
|
+
class Engine
|
51
|
+
attr_reader :roles, :role_titles, :role_descriptions, :privileges,
|
52
|
+
:privilege_hierarchy, :auth_rules, :role_hierarchy, :rev_priv_hierarchy
|
53
|
+
|
54
|
+
# If +reader+ is not given, a new one is created with the default
|
55
|
+
# authorization configuration of +AUTH_DSL_FILE+. If given, may be either
|
56
|
+
# a Reader object or a path to a configuration file.
|
57
|
+
def initialize (reader = nil)
|
58
|
+
if reader.nil?
|
59
|
+
begin
|
60
|
+
reader = Reader::DSLReader.load(AUTH_DSL_FILE)
|
61
|
+
rescue SystemCallError
|
62
|
+
reader = Reader::DSLReader.new
|
63
|
+
end
|
64
|
+
elsif reader.is_a?(String)
|
65
|
+
reader = Reader::DSLReader.load(reader)
|
66
|
+
end
|
67
|
+
@privileges = reader.privileges_reader.privileges
|
68
|
+
# {priv => [[priv, ctx],...]}
|
69
|
+
@privilege_hierarchy = reader.privileges_reader.privilege_hierarchy
|
70
|
+
@auth_rules = reader.auth_rules_reader.auth_rules
|
71
|
+
@roles = reader.auth_rules_reader.roles
|
72
|
+
@role_hierarchy = reader.auth_rules_reader.role_hierarchy
|
73
|
+
|
74
|
+
@role_titles = reader.auth_rules_reader.role_titles
|
75
|
+
@role_descriptions = reader.auth_rules_reader.role_descriptions
|
76
|
+
|
77
|
+
# {[priv, ctx] => [priv, ...]}
|
78
|
+
@rev_priv_hierarchy = {}
|
79
|
+
@privilege_hierarchy.each do |key, value|
|
80
|
+
value.each do |val|
|
81
|
+
@rev_priv_hierarchy[val] ||= []
|
82
|
+
@rev_priv_hierarchy[val] << key
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Returns true if privilege is met by the current user. Raises
|
88
|
+
# AuthorizationError otherwise. +privilege+ may be given with or
|
89
|
+
# without context. In the latter case, the :+context+ option is
|
90
|
+
# required.
|
91
|
+
#
|
92
|
+
# Options:
|
93
|
+
# [:+context+]
|
94
|
+
# The context part of the privilege.
|
95
|
+
# Defaults either to the +table_name+ of the given :+object+, if given.
|
96
|
+
# That is, either :+users+ for :+object+ of type User.
|
97
|
+
# Raises AuthorizationUsageError if
|
98
|
+
# context is missing and not to be infered.
|
99
|
+
# [:+object+] An context object to test attribute checks against.
|
100
|
+
# [:+skip_attribute_test+]
|
101
|
+
# Skips those attribute checks in the
|
102
|
+
# authorization rules. Defaults to false.
|
103
|
+
# [:+user+]
|
104
|
+
# The user to check the authorization for.
|
105
|
+
# Defaults to Authorization#current_user.
|
106
|
+
#
|
107
|
+
def permit! (privilege, options = {})
|
108
|
+
return true if Authorization.ignore_access_control
|
109
|
+
options = {
|
110
|
+
:object => nil,
|
111
|
+
:skip_attribute_test => false,
|
112
|
+
:context => nil
|
113
|
+
}.merge(options)
|
114
|
+
|
115
|
+
# Make sure we're handling all privileges as symbols.
|
116
|
+
privilege = privilege.is_a?( Array ) ?
|
117
|
+
privilege.flatten.collect { |priv| priv.to_sym } :
|
118
|
+
privilege.to_sym
|
119
|
+
|
120
|
+
#
|
121
|
+
# If the object responds to :proxy_reflection, we're probably working with
|
122
|
+
# an association proxy. Use 'new' to leverage ActiveRecord's builder
|
123
|
+
# functionality to obtain an object against which we can check permissions.
|
124
|
+
#
|
125
|
+
# Example: permit!( :edit, :object => user.posts )
|
126
|
+
#
|
127
|
+
if options[:object].respond_to?( :proxy_reflection )
|
128
|
+
options[:object] = options[:object].new
|
129
|
+
end
|
130
|
+
|
131
|
+
options[:context] ||= options[:object] && options[:object].class.table_name.to_sym rescue NoMethodError
|
132
|
+
|
133
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
134
|
+
|
135
|
+
# find a authorization rule that matches for at least one of the roles and
|
136
|
+
# at least one of the given privileges
|
137
|
+
attr_validator = AttributeValidator.new(self, user, options[:object])
|
138
|
+
rules = matching_auth_rules(roles, privileges, options[:context])
|
139
|
+
if rules.empty?
|
140
|
+
raise NotAuthorized, "No matching rules found for #{privilege} for #{user.inspect} " +
|
141
|
+
"(roles #{roles.inspect}, privileges #{privileges.inspect}, " +
|
142
|
+
"context #{options[:context].inspect})."
|
143
|
+
end
|
144
|
+
|
145
|
+
# Test each rule in turn to see whether any one of them is satisfied.
|
146
|
+
grant_permission = rules.any? do |rule|
|
147
|
+
begin
|
148
|
+
options[:skip_attribute_test] or
|
149
|
+
rule.attributes.empty? or
|
150
|
+
rule.attributes.any? do |attr|
|
151
|
+
begin
|
152
|
+
attr.validate?( attr_validator )
|
153
|
+
rescue NilAttributeValueError => e
|
154
|
+
nil # Bumping up against a nil attribute value flunks the rule.
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
unless grant_permission
|
160
|
+
raise AttributeAuthorizationError, "#{privilege} not allowed for #{user.inspect} on #{options[:object].inspect}."
|
161
|
+
end
|
162
|
+
true
|
163
|
+
end
|
164
|
+
|
165
|
+
# Calls permit! but rescues the AuthorizationException and returns false
|
166
|
+
# instead. If no exception is raised, permit? returns true and yields
|
167
|
+
# to the optional block.
|
168
|
+
def permit? (privilege, options = {}, &block) # :yields:
|
169
|
+
permit!(privilege, options)
|
170
|
+
yield if block_given?
|
171
|
+
true
|
172
|
+
rescue NotAuthorized
|
173
|
+
false
|
174
|
+
end
|
175
|
+
|
176
|
+
# Returns the obligations to be met by the current user for the given
|
177
|
+
# privilege as an array of obligation hashes in form of
|
178
|
+
# [{:object_attribute => obligation_value, ...}, ...]
|
179
|
+
# where +obligation_value+ is either (recursively) another obligation hash
|
180
|
+
# or a value spec, such as
|
181
|
+
# [operator, literal_value]
|
182
|
+
# The obligation hashes in the array should be OR'ed, conditions inside
|
183
|
+
# the hashes AND'ed.
|
184
|
+
#
|
185
|
+
# Example
|
186
|
+
# {:branch => {:company => [:is, 24]}, :active => [:is, true]}
|
187
|
+
#
|
188
|
+
# Options
|
189
|
+
# [:+context+] See permit!
|
190
|
+
# [:+user+] See permit!
|
191
|
+
#
|
192
|
+
def obligations (privilege, options = {})
|
193
|
+
options = {:context => nil}.merge(options)
|
194
|
+
user, roles, privileges = user_roles_privleges_from_options(privilege, options)
|
195
|
+
attr_validator = AttributeValidator.new(self, user)
|
196
|
+
matching_auth_rules(roles, privileges, options[:context]).collect do |rule|
|
197
|
+
obligation = rule.attributes.collect {|attr| attr.obligation(attr_validator) }
|
198
|
+
obligation.empty? ? [{}] : obligation
|
199
|
+
end.flatten
|
200
|
+
end
|
201
|
+
|
202
|
+
# Returns the description for the given role. The description may be
|
203
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
204
|
+
# given.
|
205
|
+
def description_for (role)
|
206
|
+
role_descriptions[role]
|
207
|
+
end
|
208
|
+
|
209
|
+
# Returns the title for the given role. The title may be
|
210
|
+
# specified with the authorization rules. Returns +nil+ if none was
|
211
|
+
# given.
|
212
|
+
def title_for (role)
|
213
|
+
role_titles[role]
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the role symbols of the given user.
|
217
|
+
def roles_for (user)
|
218
|
+
raise AuthorizationUsageError, "User object doesn't respond to roles" \
|
219
|
+
if !user.respond_to?(:role_symbols) and !user.respond_to?(:roles)
|
220
|
+
|
221
|
+
RAILS_DEFAULT_LOGGER.info("The use of user.roles is deprecated. Please add a method " +
|
222
|
+
"role_symbols to your User model.") if defined?(RAILS_DEFAULT_LOGGER) and !user.respond_to?(:role_symbols)
|
223
|
+
|
224
|
+
roles = user.respond_to?(:role_symbols) ? user.role_symbols : user.roles
|
225
|
+
|
226
|
+
raise AuthorizationUsageError, "User.#{user.respond_to?(:role_symbols) ? 'role_symbols' : 'roles'} " +
|
227
|
+
"doesn't return an Array of Symbols (#{roles.inspect})" \
|
228
|
+
if !roles.is_a?(Array) or (!roles.empty? and !roles[0].is_a?(Symbol))
|
229
|
+
|
230
|
+
(roles.empty? ? [:guest] : roles)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns an instance of Engine, which is created if there isn't one
|
234
|
+
# yet. If +dsl_file+ is given, it is passed on to Engine.new and
|
235
|
+
# a new instance is always created.
|
236
|
+
def self.instance (dsl_file = nil)
|
237
|
+
if dsl_file or ENV['RAILS_ENV'] == 'development'
|
238
|
+
@@instance = new(dsl_file)
|
239
|
+
else
|
240
|
+
@@instance ||= new
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
class AttributeValidator # :nodoc:
|
245
|
+
attr_reader :user, :object, :engine
|
246
|
+
def initialize (engine, user, object = nil)
|
247
|
+
@engine = engine
|
248
|
+
@user = user
|
249
|
+
@object = object
|
250
|
+
end
|
251
|
+
|
252
|
+
def evaluate (value_block)
|
253
|
+
# TODO cache?
|
254
|
+
instance_eval(&value_block)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
private
|
259
|
+
def user_roles_privleges_from_options(privilege, options)
|
260
|
+
options = {
|
261
|
+
:user => nil,
|
262
|
+
:context => nil
|
263
|
+
}.merge(options)
|
264
|
+
user = options[:user] || Authorization.current_user
|
265
|
+
privileges = privilege.is_a?(Array) ? privilege : [privilege]
|
266
|
+
|
267
|
+
raise AuthorizationUsageError, "No user object given (#{user.inspect})" \
|
268
|
+
unless user
|
269
|
+
|
270
|
+
roles = flatten_roles(roles_for(user))
|
271
|
+
privileges = flatten_privileges privileges, options[:context]
|
272
|
+
[user, roles, privileges]
|
273
|
+
end
|
274
|
+
|
275
|
+
def flatten_roles (roles)
|
276
|
+
# TODO caching?
|
277
|
+
flattened_roles = roles.clone.to_a
|
278
|
+
flattened_roles.each do |role|
|
279
|
+
flattened_roles.concat(@role_hierarchy[role]).uniq! if @role_hierarchy[role]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
# Returns the privilege hierarchy flattened for given privileges in context.
|
284
|
+
def flatten_privileges (privileges, context = nil)
|
285
|
+
# TODO caching?
|
286
|
+
#if context.nil?
|
287
|
+
# context = privileges.collect { |p| p.to_s.split('_') }.
|
288
|
+
# reject { |p_p| p_p.length < 2 }.
|
289
|
+
# collect { |p_p| (p_p[1..-1] * '_').to_sym }.first
|
290
|
+
# raise AuthorizationUsageError, "No context given or inferable from privileges #{privileges.inspect}" unless context
|
291
|
+
#end
|
292
|
+
raise AuthorizationUsageError, "No context given or inferable from object" unless context
|
293
|
+
#context_regex = Regexp.new "_#{context}$"
|
294
|
+
# TODO work with contextless privileges
|
295
|
+
#flattened_privileges = privileges.collect {|p| p.to_s.sub(context_regex, '')}
|
296
|
+
flattened_privileges = privileges.clone #collect {|p| p.to_s.end_with?(context.to_s) ?
|
297
|
+
# p : [p, "#{p}_#{context}".to_sym] }.flatten
|
298
|
+
flattened_privileges.each do |priv|
|
299
|
+
flattened_privileges.concat(@rev_priv_hierarchy[[priv, nil]]).uniq! if @rev_priv_hierarchy[[priv, nil]]
|
300
|
+
flattened_privileges.concat(@rev_priv_hierarchy[[priv, context]]).uniq! if @rev_priv_hierarchy[[priv, context]]
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def matching_auth_rules (roles, privileges, context)
|
305
|
+
@auth_rules.select {|rule| rule.matches? roles, privileges, context}
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
class AuthorizationRule
|
310
|
+
attr_reader :attributes, :contexts, :role, :privileges
|
311
|
+
|
312
|
+
def initialize (role, privileges = [], contexts = nil)
|
313
|
+
@role = role
|
314
|
+
@privileges = Set.new(privileges)
|
315
|
+
@contexts = Set.new((contexts && !contexts.is_a?(Array) ? [contexts] : contexts))
|
316
|
+
@attributes = []
|
317
|
+
end
|
318
|
+
|
319
|
+
def append_privileges (privs)
|
320
|
+
@privileges.merge(privs)
|
321
|
+
end
|
322
|
+
|
323
|
+
def append_attribute (attribute)
|
324
|
+
@attributes << attribute
|
325
|
+
end
|
326
|
+
|
327
|
+
def matches? (roles, privs, context = nil)
|
328
|
+
roles = [roles] unless roles.is_a?(Array)
|
329
|
+
@contexts.include?(context) and roles.include?(@role) and
|
330
|
+
not (@privileges & privs).empty?
|
331
|
+
end
|
332
|
+
|
333
|
+
def to_long_s
|
334
|
+
attributes.collect {|attr| attr.to_long_s } * "; "
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
class Attribute
|
339
|
+
# attr_conditions_hash of form
|
340
|
+
# { :object_attribute => [operator, value_block], ... }
|
341
|
+
# { :object_attribute => { :attr => ... } }
|
342
|
+
def initialize (conditions_hash)
|
343
|
+
@conditions_hash = conditions_hash
|
344
|
+
end
|
345
|
+
|
346
|
+
def validate? (attr_validator, object = nil, hash = nil)
|
347
|
+
object ||= attr_validator.object
|
348
|
+
return false unless object
|
349
|
+
|
350
|
+
(hash || @conditions_hash).all? do |attr, value|
|
351
|
+
attr_value = object_attribute_value(object, attr)
|
352
|
+
if value.is_a?(Hash)
|
353
|
+
if attr_value.is_a?(Array)
|
354
|
+
raise AuthorizationUsageError, "Unable evaluate multiple attributes " +
|
355
|
+
"on a collection. Cannot use '=>' operator on #{attr.inspect} " +
|
356
|
+
"(#{attr_value.inspect}) for attributes #{value.inspect}."
|
357
|
+
elsif attr_value.nil?
|
358
|
+
raise NilAttributeValueError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
359
|
+
end
|
360
|
+
validate?(attr_validator, attr_value, value)
|
361
|
+
elsif value.is_a?(Array) and value.length == 2
|
362
|
+
evaluated = if value[1].is_a?(Proc)
|
363
|
+
attr_validator.evaluate(value[1])
|
364
|
+
else
|
365
|
+
value[1]
|
366
|
+
end
|
367
|
+
case value[0]
|
368
|
+
when :is
|
369
|
+
attr_value == evaluated
|
370
|
+
when :is_not
|
371
|
+
attr_value != evaluated
|
372
|
+
when :contains
|
373
|
+
attr_value.include?(evaluated)
|
374
|
+
when :does_not_contain
|
375
|
+
!attr_value.include?(evaluated)
|
376
|
+
when :is_in
|
377
|
+
evaluated.include?(attr_value)
|
378
|
+
when :is_not_in
|
379
|
+
!evaluated.include?(attr_value)
|
380
|
+
else
|
381
|
+
raise AuthorizationError, "Unknown operator #{value[0]}"
|
382
|
+
end
|
383
|
+
else
|
384
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
# resolves all the values in condition_hash
|
390
|
+
def obligation (attr_validator, hash = nil)
|
391
|
+
hash = (hash || @conditions_hash).clone
|
392
|
+
hash.each do |attr, value|
|
393
|
+
if value.is_a?(Hash)
|
394
|
+
hash[attr] = obligation(attr_validator, value)
|
395
|
+
elsif value.is_a?(Array) and value.length == 2
|
396
|
+
hash[attr] = [value[0], attr_validator.evaluate(value[1])]
|
397
|
+
else
|
398
|
+
raise AuthorizationError, "Wrong conditions hash format"
|
399
|
+
end
|
400
|
+
end
|
401
|
+
hash
|
402
|
+
end
|
403
|
+
|
404
|
+
def to_long_s (hash = nil)
|
405
|
+
if hash
|
406
|
+
hash.inject({}) do |memo, key_val|
|
407
|
+
key, val = key_val
|
408
|
+
memo[key] = case val
|
409
|
+
when Array then "#{val[0]} { #{val[1].respond_to?(:to_ruby) ? val[1].to_ruby.gsub(/^proc \{\n?(.*)\n?\}$/m, '\1') : "..."} }"
|
410
|
+
when Hash then to_long_s(val)
|
411
|
+
end
|
412
|
+
memo
|
413
|
+
end
|
414
|
+
else
|
415
|
+
"if_attribute #{to_long_s(@conditions_hash).inspect}"
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
protected
|
420
|
+
def object_attribute_value (object, attr)
|
421
|
+
begin
|
422
|
+
object.send(attr)
|
423
|
+
rescue ArgumentError, NoMethodError => e
|
424
|
+
raise AuthorizationUsageError, "Error when calling #{attr} on " +
|
425
|
+
"#{object.inspect} for validating attribute: #{e}"
|
426
|
+
end
|
427
|
+
end
|
428
|
+
end
|
429
|
+
|
430
|
+
# An attribute condition that uses existing rules to decide validation
|
431
|
+
# and create obligations.
|
432
|
+
class AttributeWithPermission < Attribute
|
433
|
+
# E.g. privilege :read, attr_or_hash either :attribute or
|
434
|
+
# { :attribute => :deeper_attribute }
|
435
|
+
def initialize (privilege, attr_or_hash, context = nil)
|
436
|
+
@privilege = privilege
|
437
|
+
@context = context
|
438
|
+
@attr_hash = attr_or_hash
|
439
|
+
end
|
440
|
+
|
441
|
+
def validate? (attr_validator, object = nil, hash_or_attr = nil)
|
442
|
+
object ||= attr_validator.object
|
443
|
+
hash_or_attr ||= @attr_hash
|
444
|
+
return false unless object
|
445
|
+
|
446
|
+
case hash_or_attr
|
447
|
+
when Symbol
|
448
|
+
attr_value = object_attribute_value(object, hash_or_attr)
|
449
|
+
attr_validator.engine.permit? @privilege, :object => attr_value, :user => attr_validator.user
|
450
|
+
when Hash
|
451
|
+
hash_or_attr.all? do |attr, sub_hash|
|
452
|
+
attr_value = object_attribute_value(object, attr)
|
453
|
+
if attr_value.nil?
|
454
|
+
raise AuthorizationError, "Attribute #{attr.inspect} is nil in #{object.inspect}."
|
455
|
+
end
|
456
|
+
validate?(attr_validator, attr_value, sub_hash)
|
457
|
+
end
|
458
|
+
else
|
459
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
# may return an array of obligations to be OR'ed
|
464
|
+
def obligation (attr_validator, hash_or_attr = nil)
|
465
|
+
hash_or_attr ||= @attr_hash
|
466
|
+
case hash_or_attr
|
467
|
+
when Symbol
|
468
|
+
obligations = attr_validator.engine.obligations(@privilege,
|
469
|
+
:context => @context || hash_or_attr.to_s.pluralize.to_sym,
|
470
|
+
:user => attr_validator.user)
|
471
|
+
obligations.collect {|obl| {hash_or_attr => obl} }
|
472
|
+
when Hash
|
473
|
+
obligations_array_attrs = []
|
474
|
+
obligations =
|
475
|
+
hash_or_attr.inject({}) do |all, pair|
|
476
|
+
attr, sub_hash = pair
|
477
|
+
all[attr] = obligation(attr_validator, sub_hash)
|
478
|
+
if all[attr].length > 1
|
479
|
+
obligations_array_attrs << attr
|
480
|
+
else
|
481
|
+
all[attr] = all[attr].first
|
482
|
+
end
|
483
|
+
all
|
484
|
+
end
|
485
|
+
obligations = [obligations]
|
486
|
+
obligations_array_attrs.each do |attr|
|
487
|
+
next_array_size = obligations.first[attr].length
|
488
|
+
obligations = obligations.collect do |obls|
|
489
|
+
(0...next_array_size).collect do |idx|
|
490
|
+
obls_wo_array = obls.clone
|
491
|
+
obls_wo_array[attr] = obls_wo_array[attr][idx]
|
492
|
+
obls_wo_array
|
493
|
+
end
|
494
|
+
end.flatten
|
495
|
+
end
|
496
|
+
obligations
|
497
|
+
else
|
498
|
+
raise AuthorizationError, "Wrong conditions hash format: #{hash_or_attr.inspect}"
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def to_long_s
|
503
|
+
"if_permitted_to #{@privilege.inspect}, #{@attr_hash.inspect}"
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# Represents a pseudo-user to facilitate guest users in applications
|
508
|
+
class GuestUser
|
509
|
+
attr_reader :role_symbols
|
510
|
+
def initialize (roles = [:guest])
|
511
|
+
@role_symbols = roles
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|