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 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', /()(=&gt;)/, /()(\{)/, /()(\})/, /()(\[)/, /()(\])/],
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,6 @@
1
+ ActionController::Routing::Routes.draw do |map|
2
+ if Authorization::activate_authorization_rules_browser?
3
+ map.resources :authorization_rules, :only => :index, :collection => {:graph => :get}
4
+ map.resources :authorization_usages, :only => :index
5
+ end
6
+ end
@@ -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