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 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