-
Notifications
You must be signed in to change notification settings - Fork 1
/
README
163 lines (135 loc) · 11.1 KB
/
README
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
Authorize
=========
Authorize is a Ruby on Rails plugin providing a sophisticated Role-Based Access Control (RBAC) system. Current functionality highlights include:
* Polymorphic association of ActiveRecord models as authorizable resources.
* Three-level (global, class, instance) authorizations over resources.
* "Acts" to support a single ActiveRecord model being both an authorizable subject and trustee.
* Hierarchical role tree supporting rich modelling of role assignment.
* High-performance resolution of effective roles using Redis key-value database as a graph database.
For more information on the theory of RBAC, see http://en.wikipedia.org/wiki/Role-based_access_control
----------------
ActionPack
The Authorize plugin extends ActionController and ActionView with the ability to check permissions and react accordingly. There are two approaches:
a simple boolean check (permit?) and a more sophisticated predicated block (permit) with a configurable callback. In both cases, the method accepts
a permissions description hash. For example, using the boolean version:
permit?(:update => widget)
More complex expressions typically involve checking for permissions to multiple model instances. The following predicate, for example,
is true if the current roles include the :all permission over foo OR the :read permission over bar:
permit?({:all => foo, :read => bar})
Clean hooks are available for identifying the appropriate roles for the current request. No "User" class is assumed, only a
ApplicationController#roles method that returns an enumeration of the roles for the current request. A simple implementation might
work something like this:
class ApplicationController << ActionController::Base
def roles
User.current.role.roles
end
end
----------------
ActiveRecord
The Authorize plugin extends ActiveRecord with two methods: authorizable_resource and authorizable_trustee. A given model may invoke
either or both, depending on requirements. Models are thus extended with additional capabilities as follows:
Trustee
A #role association is defined that links a trustee to a "primary" or "identity" role (Authorize::Role). This role serves as the entry
point for traversing the role hierarchy and determining the effective set of roles (identity role plus its children) for a given trustee.
Resource
A #permissions association is defined that links a resource to the set of permissions (Authorize::Permission) that define the available
access modes to the resource. A #permitted parameterized scope is also provided:
Resource.permitted(roles, options = {})
Returns the resources to which the given roles have any permissions. Optionally, the qualifying permissions can be restricted with the
remaining arguments, or the :mode or :modes options:
Examples:
Widget.permitted([Authorize::Role::PUB])
Widget.permitted([Authorize::Role::PUB, my_role], :mode => :update)
Widget.permitted([Authorize::Role::PUB, my_role], :modes => [:delete, :update])
Widget.permitted([Authorize::Role::PUB, my_role], :delete, :update)
In addition to the macro methods described above, two models (ActiveRecord::Base subclasses) are defined:
Authorize::Permission
Permissions link roles to resources along with a defined access mode. Access modes are limited to the classics (read, update, delete, etc.),
but interpretation of them is application-specific (but see the note below about the list mode). Permissions apply to resources at one of
three levels:
Instance (e.g. update permission for the Widget instance with id 6324)
Class (e.g. list permission for all instances of Widget)
Global (e.g. read permission for all instances of every model class)
NOTE: To maximize performance, the list access mode is assumed to be included in EVERY instance of Authorize::Permission. This allows
efficient SQL joins with model tables (widgets, for example) and the permissions table (authorize_permissions). The permissions table
can grow quite large, and this implied mode obviates the need to index the mask field and add complex conditions to permissions queries.
Authorize::Role
Roles allow flexible modelling of application-specific functions. The Role class is rather thin and mainly serves to identify a role
instance and polymorphically associate it, where appropriate, to a trustee. Critically, it has the sole interface (#roles) into the
role graph (a DAG) stored in the Redis database.
----------------
A Note on Performance
Performance of the Resource.permitted named scope is critical to effective use of this plugin. However, it is difficult to optimize across multiple
database systems for multiple use cases. Empirically, it seems to be best to use a UNION of the three cases that can yield a permission: global, class-
based and instance based. Alternatives using moderately complex nested or expanded OR clauses fail to optimize correctly on MySQL 5.0 and degrade terribly
with substantial authorization and subject volume. Not surprisingly, COALESCE also fails to optimize nicely. A JOIN-based solution was considered, but the
semantics of a JOIN are such that duplicate subject records are returned. The duplicates could be eliminated with :group and :having options, but at the cost
of transparency of the #permitted named scope.
Indexing of the authorize_permissions table is very important. See the test application's schema for an reasonable set of indices.
Code examples of alternatives:
# Baseline with nested booleans
c1 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_type => nil)
c2 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_type => base_class.name)
c3l = "%s.%s" % [reflection.quoted_table_name, connection.quote_column_name(reflection.primary_key_name)]
c3r = "%s.%s" % [connection.quote_table_name(table_name), connection.quote_column_name(primary_key)]
c4 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_id => nil)
subject_condition_clause = "#{c1} OR (#{c2} AND (#{c3l} = #{c3r} OR #{c4}))"
named_scope :a0, lambda {|tokens, roles|
scope = Authorize::Permission.scoped(:conditions => subject_condition_clause).with(tokens).as(roles)
c = scope.construct_finder_sql({:select => 1, :from => "#{reflection.quoted_table_name} a"}).gsub(/#{reflection.quoted_table_name}\./, 'a.')
{:conditions => "EXISTS (%s)" % c}
}
# Baseline with booleans expanded into three ORs
c1 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_type => nil)
c2 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_type => base_class.name)
c3l = "%s.%s" % [reflection.quoted_table_name, connection.quote_column_name(reflection.primary_key_name)]
c3r = "%s.%s" % [connection.quote_table_name(table_name), connection.quote_column_name(primary_key)]
c4 = Authorize::Permission.sanitize_sql_hash_for_conditions(:subject_id => nil)
subject_condition_clause = "#{c1} OR (#{c2} AND #{c3l} = #{c3r}) OR (#{c1} AND #{c4})"
named_scope :a1, lambda {|tokens, roles|
scope = Authorize::Permission.scoped(:conditions => subject_condition_clause).with(tokens).as(roles)
c = scope.construct_finder_sql({:select => 1, :from => "#{reflection.quoted_table_name} a"}).gsub(/#{reflection.quoted_table_name}\./, 'a.')
{:conditions => "EXISTS (%s)" % c}
}
# COALESCE replacing OR (and a subtle but harmless semantic shift)
auth_fk = "#{reflection.quoted_table_name}.#{connection.quote_column_name(reflection.primary_key_name)}"
subject_pk = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(primary_key)}"
auth_fk_type = "#{reflection.quoted_table_name}.#{connection.quote_column_name(Authorize::Permission.reflections[:subject].options[:foreign_type])}"
subject_condition_clause = "%s = COALESCE(#{auth_fk_type}, %s) AND #{subject_pk} = COALESCE(#{auth_fk}, #{subject_pk})" % ([connection.quote(base_class.name)] * 2)
named_scope :a2, lambda {|tokens, roles|
scope = Authorize::Permission.scoped(:conditions => subject_condition_clause).with(tokens).as(roles)
c = scope.construct_finder_sql({:select => 1, :from => "#{reflection.quoted_table_name} a"}).gsub(/#{reflection.quoted_table_name}\./, 'a.')
{:conditions => "EXISTS (%s)" % c}
}
# Correlated subquery with COALESCE
auth_fk = "#{reflection.quoted_table_name}.#{connection.quote_column_name(reflection.primary_key_name)}"
subject_pk = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(primary_key)}"
auth_fk_type = "#{reflection.quoted_table_name}.#{connection.quote_column_name(Authorize::Permission.reflections[:subject].options[:foreign_type])}"
subject_condition_clause = "%s = COALESCE(#{auth_fk_type}, %s)" % ([connection.quote(base_class.name)] * 2)
select_clause = "COALESCE(#{auth_fk}, #{subject_pk})"
named_scope :a3, lambda {|tokens, roles|
scope = Authorize::Permission.scoped(:conditions => subject_condition_clause).with(tokens).as(roles)
c = scope.construct_finder_sql({:select => select_clause})
{:conditions => "#{subject_pk} IN (#{c})"}
}
# Three-way union - nice performance but UGLY query
auth_fk = "#{reflection.quoted_table_name}.#{connection.quote_column_name(reflection.primary_key_name)}"
subject_pk = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(primary_key)}"
named_scope :a4, lambda {|tokens, roles|
scope = Authorize::Permission.with(tokens).as(roles)
sq0 = scope.construct_finder_sql({:select => true, :conditions => {:subject_id => nil, :subject_type => nil}})
sq1 = scope.construct_finder_sql({:select => true, :conditions => {:subject_type => base_class.name, :subject_id => nil}})
sq2 = scope.scoped(:conditions => "#{auth_fk} = #{subject_pk}").construct_finder_sql({:select => true, :conditions => {:subject_type => base_class.name}})
{:conditions => "EXISTS (#{sq0} UNION #{sq1} UNION #{sq2})"}
}
# Join - possible to get nice performance, but semantics collapse
auth_fk = "#{reflection.quoted_table_name}.#{connection.quote_column_name(reflection.primary_key_name)}"
subject_pk = "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(primary_key)}"
auth_fk_type = "#{reflection.quoted_table_name}.#{connection.quote_column_name(Authorize::Permission.reflections[:subject].options[:foreign_type])}"
subject_condition_clause = "%s = COALESCE(#{auth_fk_type}, %s) AND #{subject_pk} = COALESCE(#{auth_fk}, #{subject_pk})" % ([connection.quote(base_class.name)] * 2)
named_scope :a9, lambda {|tokens, roles|
ascope = Authorize::Permission.with(tokens).as(roles).current_scoped_methods[:find][:conditions]
{:joins => "JOIN authorizations ON #{subject_condition_clause}", :conditions => {:authorizations => ascope}}
}
TODO:
* Flexible configuration of permission bits