stffn-declarative_authorization 0.2.1 → 0.2.3
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/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
|