model_security_generator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +0 -0
- data/USAGE +0 -0
- data/model_security_generator.rb +75 -0
- data/templates/_view_form.rhtml +27 -0
- data/templates/mailer_forgot_password.rhtml +18 -0
- data/templates/mailer_new_user.rhtml +10 -0
- data/templates/mock_mailer.rb +16 -0
- data/templates/mock_time.rb +17 -0
- data/templates/modal.rb +82 -0
- data/templates/modal_helper.rb +29 -0
- data/templates/model_security.rb +334 -0
- data/templates/model_security_helper.rb +64 -0
- data/templates/once.rb +36 -0
- data/templates/scaffold.css +74 -0
- data/templates/scaffold.rhtml +11 -0
- data/templates/schema.sql +4 -0
- data/templates/standard.css +7 -0
- data/templates/standard.rhtml +16 -0
- data/templates/user.rb +328 -0
- data/templates/user_controller.rb +178 -0
- data/templates/user_controller_test.rb +20 -0
- data/templates/user_mailer.rb +29 -0
- data/templates/user_support.rb +124 -0
- data/templates/user_test.rb +10 -0
- data/templates/users.sql +17 -0
- data/templates/users.yml +41 -0
- data/templates/view_activate.rhtml +1 -0
- data/templates/view_edit.rhtml +10 -0
- data/templates/view_forgot_password_done.rhtml +5 -0
- data/templates/view_list.rhtml +35 -0
- data/templates/view_login.rhtml +11 -0
- data/templates/view_login_admin.rhtml +16 -0
- data/templates/view_logout.rhtml +1 -0
- data/templates/view_new.rhtml +30 -0
- data/templates/view_show.rhtml +10 -0
- data/templates/view_success.rhtml +1 -0
- metadata +83 -0
data/README
ADDED
File without changes
|
data/USAGE
ADDED
File without changes
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class ModelSecurityGenerator < Rails::Generator::NamedBase
|
2
|
+
def manifest
|
3
|
+
record do |m|
|
4
|
+
# Check for class naming collisions.
|
5
|
+
m.class_collisions class_path, "User", "UserController", "UserMailer", "UserSupport", "Modal", "ModalHelper", "ModelSecurity", "ModelSecurityHelper"
|
6
|
+
|
7
|
+
# Libraries
|
8
|
+
m.file "modal.rb", "lib/modal.rb"
|
9
|
+
m.file "model_security.rb", "lib/model_security.rb"
|
10
|
+
m.file "once.rb", "lib/once.rb"
|
11
|
+
m.file "user_support.rb", "lib/user_support.rb"
|
12
|
+
|
13
|
+
# Helpers
|
14
|
+
m.file "modal_helper.rb", "app/helpers/modal_helper.rb"
|
15
|
+
m.file "model_security_helper.rb", "app/helpers/model_security_helper.rb"
|
16
|
+
|
17
|
+
# User
|
18
|
+
m.file "user.rb", File.join("app/models", "user.rb")
|
19
|
+
m.file "user_controller.rb", File.join("app/controllers", "user_controller.rb")
|
20
|
+
|
21
|
+
# User mailer
|
22
|
+
m.file "user_mailer.rb", File.join("app/models", "user_mailer.rb")
|
23
|
+
|
24
|
+
# Testing related stuff
|
25
|
+
m.file "user_controller_test.rb", "test/functional/user_controller_test.rb"
|
26
|
+
m.file "user_test.rb", "test/unit/user_test.rb"
|
27
|
+
m.file "mock_mailer.rb", "test/mocks/test/user_mailer.rb"
|
28
|
+
m.file "mock_time.rb", "test/mocks/test/time.rb"
|
29
|
+
m.file "users.yml", "test/fixtures/users.yml"
|
30
|
+
|
31
|
+
# Schemas, configuration and miscellaneous
|
32
|
+
m.file "schema.sql", "db/schema.sql"
|
33
|
+
m.file "users.sql", "db/users.sql"
|
34
|
+
|
35
|
+
# Layout and stylesheet.
|
36
|
+
m.file "scaffold.rhtml", "app/views/layouts/scaffold.rhtml"
|
37
|
+
m.file "scaffold.css", "public/stylesheets/scaffold.css"
|
38
|
+
m.file "standard.rhtml", "app/views/layouts/standard.rhtml"
|
39
|
+
m.file "standard.css", "public/stylesheets/standard.css"
|
40
|
+
|
41
|
+
# Views
|
42
|
+
m.directory File.join("app/views", "user", file_name)
|
43
|
+
user_views.each do |action|
|
44
|
+
m.file "view_#{action}.rhtml",
|
45
|
+
File.join("app/views", "user", file_name, "#{action}.rhtml")
|
46
|
+
end
|
47
|
+
|
48
|
+
# Partials
|
49
|
+
m.directory File.join("app/views", "user", file_name)
|
50
|
+
partial_views.each do |action|
|
51
|
+
m.file "_view_#{action}.rhtml",
|
52
|
+
File.join("app/views", "user", file_name, "_#{action}.rhtml")
|
53
|
+
end
|
54
|
+
|
55
|
+
# Mailer
|
56
|
+
m.directory File.join("app/views", "user_mailer")
|
57
|
+
mailer_views.each do |action|
|
58
|
+
m.file "mailer_#{action}.rhtml",
|
59
|
+
File.join("app/views", "user_mailer", "#{action}.rhtml")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def user_views
|
65
|
+
%w(activate edit forgot_password_done list login login_admin logout new show success)
|
66
|
+
end
|
67
|
+
|
68
|
+
def partial_views
|
69
|
+
%w(form)
|
70
|
+
end
|
71
|
+
|
72
|
+
def mailer_views
|
73
|
+
%w(forgot_password new_user)
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
<%= error_messages_for 'user' %>
|
2
|
+
|
3
|
+
<!--[form:user]-->
|
4
|
+
<table>
|
5
|
+
<% for column in User.content_columns %>
|
6
|
+
<% if User.display?(column.name) %>
|
7
|
+
<tr>
|
8
|
+
<th>
|
9
|
+
<label for="user_#{column.name}">
|
10
|
+
<%= column.human_name %>:
|
11
|
+
</label>
|
12
|
+
</th>
|
13
|
+
<td>
|
14
|
+
<% if column.name.index('password') %>
|
15
|
+
<%= password_field('user', column.name) %>
|
16
|
+
<% else %>
|
17
|
+
<%= text_field('user', column.name) %>
|
18
|
+
<% end %>
|
19
|
+
</td>
|
20
|
+
</tr>
|
21
|
+
<% end %>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
</table>
|
25
|
+
|
26
|
+
<!--[eoform:user]-->
|
27
|
+
|
@@ -0,0 +1,18 @@
|
|
1
|
+
<h1>Recover Your Login or Re-Set Your Password</h1>
|
2
|
+
|
3
|
+
<p>
|
4
|
+
Please enter the email address you used when you registered your login.
|
5
|
+
A message will be sent to that email with the information you'll need to
|
6
|
+
reset your password.
|
7
|
+
</p>
|
8
|
+
<%= start_form_tag :action => 'forgot_password' %>
|
9
|
+
<table>
|
10
|
+
<tr>
|
11
|
+
<th><label for="user_email">Email</label></th>
|
12
|
+
<td><%= text_field 'user', 'email' %></td>
|
13
|
+
</tr>
|
14
|
+
</table>
|
15
|
+
<%= submit_tag "Send Email" %>
|
16
|
+
<%= end_form_tag %>
|
17
|
+
|
18
|
+
<%= link_to 'Back', :action => 'list' %>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'models/user_mailer.rb'
|
2
|
+
|
3
|
+
ActionMailer::Base.class_eval {
|
4
|
+
@@inject_one_error = false
|
5
|
+
cattr_accessor :inject_one_error
|
6
|
+
|
7
|
+
private
|
8
|
+
def perform_delivery_test(mail)
|
9
|
+
if inject_one_error
|
10
|
+
ActionMailer::Base::inject_one_error = false
|
11
|
+
raise "Failed to send email" if raise_delivery_errors
|
12
|
+
else
|
13
|
+
deliveries << mail
|
14
|
+
end
|
15
|
+
end
|
16
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
Time.class_eval {
|
4
|
+
@@advance_by_days = 0
|
5
|
+
cattr_accessor :advance_by_days
|
6
|
+
|
7
|
+
class << Time
|
8
|
+
alias now_old now
|
9
|
+
def now
|
10
|
+
if Time.advance_by_days != 0
|
11
|
+
return Time.at(now_old.to_i + Time.advance_by_days * 60 * 60 * 24 + 1)
|
12
|
+
else
|
13
|
+
now_old
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
}
|
data/templates/modal.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# Implement modal web pages. When you are done with one of these pages, they
|
2
|
+
# will redirect you to an internal page anchor within the page that linked to
|
3
|
+
# them. In other words, you don't just go back to the referring page. You go
|
4
|
+
# back to the place within the referring page where the link was.
|
5
|
+
#
|
6
|
+
# This implementation is in two parts: the library module Modal, and the view
|
7
|
+
# helper ModalHelper.
|
8
|
+
#
|
9
|
+
module Modal
|
10
|
+
|
11
|
+
# This is meant to be called as a before filter by the ApplicationController.
|
12
|
+
# It always returns true and thus will not interrupt your action.
|
13
|
+
#
|
14
|
+
# The link_modal and modal_form methods generate an internal page
|
15
|
+
# anchor and a +ret+ parameter that will be passed in a GET or PUT.
|
16
|
+
# The anchor refers the location within the calling page to return to
|
17
|
+
# after a modal action, and +ret+ encodes the URL of that anchor.
|
18
|
+
# store_return retrieves +ret+ from the request and saves it in a
|
19
|
+
# return-to address URL in the session. The return-to URL will be used
|
20
|
+
# by redirect_back_or_default to return to a location within the calling
|
21
|
+
# page that linked to the current page. This is nicer than simply returning
|
22
|
+
# to the top of the page, especially when returning to a long page like
|
23
|
+
# a list of blog comments, because the reader will probably want to continue
|
24
|
+
# reading at that point.
|
25
|
+
#
|
26
|
+
# If the request does not contain a +ret+ parameter and the return-to
|
27
|
+
# address is not set and REFERER is set in the environment, the
|
28
|
+
# return-to address is set to REFERER.
|
29
|
+
#
|
30
|
+
def modal_setup
|
31
|
+
# Set modal return.
|
32
|
+
if r = @params['ret']
|
33
|
+
self.return_location = r.sub(/\.L/, '#L')
|
34
|
+
elsif return_location.nil?
|
35
|
+
self.return_location = @request.env['HTTP_REFERER']
|
36
|
+
end
|
37
|
+
true
|
38
|
+
end
|
39
|
+
|
40
|
+
# Get the location to return to after a modal action. The argument should
|
41
|
+
# be a string containing a URL.
|
42
|
+
def return_location
|
43
|
+
@session[:return_to]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Set the location to return to after a modal action. The return value should
|
47
|
+
# be a string containing a URL.
|
48
|
+
def return_location= a
|
49
|
+
@session[:return_to] = a
|
50
|
+
end
|
51
|
+
|
52
|
+
# Store the location to return to after a modal action from the request URI.
|
53
|
+
# This is usually called before redirecting to another action.
|
54
|
+
def store_location
|
55
|
+
self.return_location = @request.request_uri
|
56
|
+
end
|
57
|
+
|
58
|
+
# Redirect to the stored return location. If no stored return location
|
59
|
+
# is available, redirect to the default action given in the arguments.
|
60
|
+
# The arguments are just like those of redirect_to.
|
61
|
+
def redirect_back_or_default(attributes = {}, *method_params)
|
62
|
+
r = return_location
|
63
|
+
if r
|
64
|
+
redirect_to_url r
|
65
|
+
self.return_location = nil
|
66
|
+
else
|
67
|
+
redirect_to attributes, method_params
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Create a URL optionally including an internal anchor. If the +id+ argument
|
72
|
+
# is given, that will be used to create the name of the internal anchor. It's
|
73
|
+
# generally a number, but anything that will convert to a string that does
|
74
|
+
# not contain characters that would have special meaning within a URL, like
|
75
|
+
# spaces, will do.
|
76
|
+
def return_url(id = nil)
|
77
|
+
s = @request.request_uri.sub(/\??ret=[^\&$]*/,'')
|
78
|
+
s += '.L' + id.to_s if id
|
79
|
+
s
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'modal'
|
2
|
+
|
3
|
+
module ModalHelper
|
4
|
+
include Modal
|
5
|
+
|
6
|
+
# Include "ret" in a form, for modal forms.
|
7
|
+
def modal_form(id = nil)
|
8
|
+
s = '<input type="hidden" name="ret" value="' + return_url(id) + '"'
|
9
|
+
s += 'id="L' + id.to_s + '"' if id
|
10
|
+
s += '/>'
|
11
|
+
s
|
12
|
+
end
|
13
|
+
|
14
|
+
# Include "ret" in a link, for modal links.
|
15
|
+
def modal_link_to(name, id = nil, options = {}, html_options = nil, *dict_params)
|
16
|
+
html_options = {} if not html_options
|
17
|
+
html_options['id'] = 'L' + id.to_s if id
|
18
|
+
html_options = html_options.stringify_keys
|
19
|
+
convert_confirm_option_to_javascript!(html_options)
|
20
|
+
if options.is_a?(String)
|
21
|
+
l = content_tag "a", name || options, html_options.merge("href" => options)
|
22
|
+
else
|
23
|
+
s = url_for(options, dict_params)
|
24
|
+
s += ('?ret=' + return_url(id))
|
25
|
+
l = content_tag("a", (name || url_for(options, dict_params)), html_options.merge("href" => s))
|
26
|
+
end
|
27
|
+
l
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,334 @@
|
|
1
|
+
# The ModelSecurity module allows you to specify security permissions on any
|
2
|
+
# or all of the attributes of a model implemented using ActiveRecord.
|
3
|
+
#
|
4
|
+
# Security permissions are
|
5
|
+
# specified in the declaration of the model's class, similarly to the way
|
6
|
+
# you can specify validators. The specification includes the names of
|
7
|
+
# attributes to which permissions apply, and an optional permission test that
|
8
|
+
# should return true or false depending on whether the access should be allowed
|
9
|
+
# or denied.
|
10
|
+
#
|
11
|
+
# let_read :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
|
12
|
+
# let_write :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
|
13
|
+
# let_access :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
|
14
|
+
#
|
15
|
+
# let_read specifies when the attribute can be read, let_write specifies when
|
16
|
+
# it can be written, and let_access does both.
|
17
|
+
#
|
18
|
+
# If no permission test is specified, that is the same as specifying a test
|
19
|
+
# that always returns true. Two stub tests are provided:
|
20
|
+
#
|
21
|
+
# :always?
|
22
|
+
#
|
23
|
+
# Always returns true.
|
24
|
+
#
|
25
|
+
# :never?
|
26
|
+
#
|
27
|
+
# Always returns false.
|
28
|
+
#
|
29
|
+
# You can easily add your own tests as instance methods of your model:
|
30
|
+
#
|
31
|
+
# let_read :phone_number :if => :admin?
|
32
|
+
#
|
33
|
+
# def admin?
|
34
|
+
# return $current_login.is_the_administrator
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# If the permission test is specified using the syntax
|
38
|
+
# :if => :test-name
|
39
|
+
# it will be run as a method of the model this way:
|
40
|
+
# self.send(:test-name)
|
41
|
+
#
|
42
|
+
# If the permission test is specified as a block, using *do* and *end*,
|
43
|
+
# it will be called with the binding of the active record instance that
|
44
|
+
# is being accessed.
|
45
|
+
#
|
46
|
+
# Permission tests can also be strings, and these are passed to eval().
|
47
|
+
#
|
48
|
+
# The special attribute name :all means that a test will be applied to all
|
49
|
+
# attributes of the model. Any tests for :all are run first, then any tests
|
50
|
+
# for the specific attribute. Any test that returns true ends the run, further
|
51
|
+
# tests will not be evaluated.
|
52
|
+
#
|
53
|
+
# If *no* security permissions are declared for an attribute or :all, that
|
54
|
+
# attribute may always be accessed. Once a test for :all is delcared, that
|
55
|
+
# test will apply to all attributes of the model.
|
56
|
+
#
|
57
|
+
# The security tests themselves may access any data with impunity. A global
|
58
|
+
# variable is used to disable further security testing while a security test
|
59
|
+
# is in progress.
|
60
|
+
#
|
61
|
+
# = Display Control
|
62
|
+
#
|
63
|
+
# A companion mechanism is used to control views, including scaffold views,
|
64
|
+
# using a syntax similar to that for security specifications:
|
65
|
+
#
|
66
|
+
# let_display :attribute|:all [[, :attribute ] ...], [:if => :test-name] [do block end]
|
67
|
+
#
|
68
|
+
# let_display is mostly useful for specifying if a table view should have a
|
69
|
+
# column for a particular attribute. Its tests must be declared as class
|
70
|
+
# methods of the model, while the tests of let_read, let_write, and
|
71
|
+
# let_access are instance methods. This is because the information declared
|
72
|
+
# by let_display is accessed before iteration over active records begins.
|
73
|
+
#
|
74
|
+
#
|
75
|
+
# = Accessing Security Test Results
|
76
|
+
#
|
77
|
+
# ModelSecurity provides two instance methods, readable? and writable?
|
78
|
+
# to inform the program if a particular attribute can be accessed. The class
|
79
|
+
# method display? will return true or false depending upon whether a
|
80
|
+
# particular attribute should be displayed. These can
|
81
|
+
# be used to modify a view so that any non-writable data will not be presented
|
82
|
+
# in an editable field. ModelSecurityHelper overloads the methods that are
|
83
|
+
# usually
|
84
|
+
# used to edit models so that they will not attempt to read or write what they
|
85
|
+
# aren't permitted and will render appropriately for the permissions on any
|
86
|
+
# model attribute. Those methods are: check_box, file_field, hidden_field,
|
87
|
+
# password_field, radio_button, text_area, text_field.
|
88
|
+
# ModelSecurityHelper also replaces the scaffold views with versions that
|
89
|
+
# never access data when not permitted to, render appropriately for the
|
90
|
+
# permissions on an attribute, and omit columns for which display? returns
|
91
|
+
# false.
|
92
|
+
#
|
93
|
+
#
|
94
|
+
# = Exceptions
|
95
|
+
#
|
96
|
+
# ActiveRecord provides two internal methods that perform normal attribute
|
97
|
+
# accesses: read_attribute, and write_attribute. These are overloaded to
|
98
|
+
# perform security testing, and will raise *SecurityError* when an unpermitted
|
99
|
+
# access is attempted.
|
100
|
+
#
|
101
|
+
module ModelSecurity
|
102
|
+
end
|
103
|
+
|
104
|
+
require 'once'
|
105
|
+
|
106
|
+
module ModelSecurity::BothClassAndInstanceMethods
|
107
|
+
private
|
108
|
+
# Run a single test.
|
109
|
+
def run_test t
|
110
|
+
case t.class.name
|
111
|
+
when 'Proc'
|
112
|
+
return t.call(binding)
|
113
|
+
when 'Symbol'
|
114
|
+
return self.send(t)
|
115
|
+
when 'String'
|
116
|
+
return eval(t)
|
117
|
+
else
|
118
|
+
return false
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# This does the permission test for readable?, writable?, and display?.
|
123
|
+
# A global variable is used to short-circuit recursion.
|
124
|
+
#
|
125
|
+
# FIX: The global variable should be replaced with a thread-local variable
|
126
|
+
# once I learn how to make one.
|
127
|
+
#
|
128
|
+
def run_tests(d, attribute)
|
129
|
+
global = d[:all]
|
130
|
+
local = d[attribute.to_sym]
|
131
|
+
result = true
|
132
|
+
|
133
|
+
if (global or local) and ($in_test_permission != true)
|
134
|
+
$in_test_permission = true
|
135
|
+
result = (run_test(global) or run_test(local))
|
136
|
+
$in_test_permission = false
|
137
|
+
end
|
138
|
+
return result
|
139
|
+
end
|
140
|
+
|
141
|
+
public
|
142
|
+
# Stub test for let_read and friends. Always returns true.
|
143
|
+
def always?
|
144
|
+
true
|
145
|
+
end
|
146
|
+
|
147
|
+
# Stub test for let_read and friends. Always returns false.
|
148
|
+
def never?
|
149
|
+
false
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
module ModelSecurity
|
154
|
+
include ModelSecurity::BothClassAndInstanceMethods
|
155
|
+
|
156
|
+
private
|
157
|
+
# This does the permission test for readable? or writable?.
|
158
|
+
def test_permission(permission, attribute)
|
159
|
+
if (d = self.class.read_inheritable_attribute(permission))
|
160
|
+
return run_tests(d, attribute)
|
161
|
+
else
|
162
|
+
return true
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# Responsible for raising an exception when an unpermitted security
|
167
|
+
# access is attempted. *permission* is :let_read or :let_write.
|
168
|
+
# *attribute* is the name of the attribute upon which an access is
|
169
|
+
# being attempted.
|
170
|
+
#
|
171
|
+
# FIX: Is exception information displayed in production mode? I put a lot
|
172
|
+
# of sensitive data in this exception.
|
173
|
+
#
|
174
|
+
def security_error(permission, attribute)
|
175
|
+
global = nil
|
176
|
+
local = nil
|
177
|
+
|
178
|
+
if (d = self.class.read_inheritable_attribute(permission))
|
179
|
+
global = d[:all]
|
180
|
+
local = d[attribute.to_sym]
|
181
|
+
end
|
182
|
+
|
183
|
+
message = "SECURITY VIOLATON: #{permission} on attribute #{attribute}" \
|
184
|
+
"\n\tof object: #{self.inspect}."
|
185
|
+
|
186
|
+
if global
|
187
|
+
message << "\n\tTest for :all is #{global.inspect}."
|
188
|
+
end
|
189
|
+
|
190
|
+
if local
|
191
|
+
message << "\n\tTest for :#{attribute} is #{local.inspect}."
|
192
|
+
end
|
193
|
+
|
194
|
+
raise SecurityError.new(message)
|
195
|
+
end
|
196
|
+
|
197
|
+
public
|
198
|
+
# Ruby interpreter magic to cause the class methods herein to work correctly
|
199
|
+
# while a class including this module is still being declared.
|
200
|
+
def self.append_features(base)
|
201
|
+
super
|
202
|
+
base.extend(ClassMethods)
|
203
|
+
end
|
204
|
+
|
205
|
+
# Return true if a read of *attribute* is permitted.
|
206
|
+
# *attribute* should be a symbol, and should be the
|
207
|
+
# name of a database field for this model.
|
208
|
+
def readable?(attribute)
|
209
|
+
test_permission(:let_read, attribute)
|
210
|
+
end
|
211
|
+
|
212
|
+
# Overloads ActiveRecord::Base#read_attribute. Read the attribute if that is
|
213
|
+
# permitted. Otherwise, throw an exception.
|
214
|
+
def read_attribute(name)
|
215
|
+
if not readable?(name)
|
216
|
+
security_error(:let_read, name)
|
217
|
+
end
|
218
|
+
old_read_attribute(name)
|
219
|
+
end
|
220
|
+
|
221
|
+
# Return true if a write of *attribute* is permitted.
|
222
|
+
# *attribute* should be a symbol, and should be the
|
223
|
+
# name of a database field for this model.
|
224
|
+
def writable?(attribute)
|
225
|
+
test_permission(:let_write, attribute)
|
226
|
+
end
|
227
|
+
|
228
|
+
# Overloads ActiveRecord::Base#write_attribute. Write the attribute if that is
|
229
|
+
# permitted. Otherwise, throw an exception.
|
230
|
+
def write_attribute(name, value)
|
231
|
+
if not writable?(name)
|
232
|
+
security_error(:let_write, name)
|
233
|
+
raise SecurityError
|
234
|
+
end
|
235
|
+
old_write_attribute(name, value)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# Class methods for ModelSecurity. They are broken out this way so that they
|
240
|
+
# can be fed to base.extend(), Ruby interpreter magic so that class methods
|
241
|
+
# of this module work while a class including it is being defined.
|
242
|
+
# Uses a Rails-internal inheritable-attribute mechanism so that this data in a
|
243
|
+
# derived class survives modification of similar data in its base class.
|
244
|
+
#
|
245
|
+
# I'm not sure that write_inheritable_attribute() is the right way to go, either
|
246
|
+
# here or in the way that validators are declared. Why not declare the data
|
247
|
+
# independently in each subclass and then use *super* to traverse the
|
248
|
+
# inheritance graph when accessing it?
|
249
|
+
#
|
250
|
+
module ModelSecurity::ClassMethods
|
251
|
+
private
|
252
|
+
include ModelSecurity::BothClassAndInstanceMethods
|
253
|
+
|
254
|
+
# Internal function where the work of let_read, let_write, let_access,
|
255
|
+
# and let_display is done. Store the tests to be run for each attribute
|
256
|
+
# in the class, to be evaluated later. *permission* is :let_read,
|
257
|
+
# :let_write, or :let_display. *arguments* is a list of attributes
|
258
|
+
# upon which security permissions are being declared and a hash
|
259
|
+
# containing all options, currently just :if . *block*, if given,
|
260
|
+
# contains a security test.
|
261
|
+
#
|
262
|
+
def let(permission, arguments, block)
|
263
|
+
attributes = [] # List of attributes that this permission applies to.
|
264
|
+
parameters = {} # Optional parameters, currently only :if
|
265
|
+
procedure = nil # Permission-test procedure.
|
266
|
+
|
267
|
+
arguments.each { | argument |
|
268
|
+
case argument.class.name
|
269
|
+
when 'Hash'
|
270
|
+
parameters.merge! argument
|
271
|
+
else
|
272
|
+
attributes << argument
|
273
|
+
end
|
274
|
+
}
|
275
|
+
if not block.nil?
|
276
|
+
procedure = block
|
277
|
+
elsif (p = parameters[:if])
|
278
|
+
procedure = p
|
279
|
+
else
|
280
|
+
procedure = :always?
|
281
|
+
end
|
282
|
+
|
283
|
+
d = {}
|
284
|
+
attributes.each { | name | d[name] = procedure }
|
285
|
+
write_inheritable_hash(permission, d)
|
286
|
+
end
|
287
|
+
|
288
|
+
public
|
289
|
+
# Return true if display of the attribute is permitted. *attribute* is a
|
290
|
+
# symbol, and should match a field in the database schema corresponding to
|
291
|
+
# this model.
|
292
|
+
def display?(attribute)
|
293
|
+
if (d = read_inheritable_attribute(:let_display))
|
294
|
+
return run_tests(d, attribute)
|
295
|
+
else
|
296
|
+
return true
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
# Declare whether reads and writes are permitted on the named attributes.
|
301
|
+
def let_access(*arguments, &block)
|
302
|
+
let(:let_read, arguments, block)
|
303
|
+
let(:let_write, arguments, block)
|
304
|
+
end
|
305
|
+
|
306
|
+
# Declare whether display of the named attribute is permitted.
|
307
|
+
def let_display(*arguments, &block)
|
308
|
+
let(:let_display, arguments, block)
|
309
|
+
end
|
310
|
+
|
311
|
+
|
312
|
+
# Declare whether read is permitted upon the named attributes.
|
313
|
+
def let_read(*arguments, &block)
|
314
|
+
let(:let_read, arguments, block)
|
315
|
+
end
|
316
|
+
|
317
|
+
# Declare whether write is permitted upon the named attributes.
|
318
|
+
def let_write(*arguments, &block)
|
319
|
+
let(:let_write, arguments, block)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
class ActiveRecord::Base
|
324
|
+
private
|
325
|
+
once ('@aliasesDone') {
|
326
|
+
# Provides access to the pre-overload version of read_attribute, which
|
327
|
+
# is called by the overloaded version.
|
328
|
+
alias old_read_attribute read_attribute
|
329
|
+
|
330
|
+
# Provides access to the pre-overload version of write_attribute, which
|
331
|
+
# is called by the overloaded version.
|
332
|
+
alias old_write_attribute write_attribute
|
333
|
+
}
|
334
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module ActionView::Helpers::FormHelper
|
2
|
+
private
|
3
|
+
|
4
|
+
def self.restrict(name)
|
5
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
6
|
+
alias old_#{name} #{name}
|
7
|
+
def #{name}(object, method, options = {})
|
8
|
+
restricted_input(:old_#{name}, object, method, options) \
|
9
|
+
end
|
10
|
+
end_eval
|
11
|
+
end
|
12
|
+
|
13
|
+
def restricted_input(old_name, object, method, options, *extra, &block)
|
14
|
+
o = instance_variable_get("@" + object)
|
15
|
+
writable = o.writable?(method)
|
16
|
+
if o.readable?(method)
|
17
|
+
if writable
|
18
|
+
if block
|
19
|
+
return yield(object, method, options, extra)
|
20
|
+
else
|
21
|
+
return send(old_name, object, method, options)
|
22
|
+
end
|
23
|
+
else
|
24
|
+
return o.send(method)
|
25
|
+
end
|
26
|
+
elseif writable
|
27
|
+
options[:value] = ''
|
28
|
+
if block
|
29
|
+
return yield(object, method, options, extra)
|
30
|
+
else
|
31
|
+
return send(old_name, object, method, options)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
once ('@aliasesDone') { # Evaluating this twice would break it.
|
37
|
+
alias old_check_box check_box
|
38
|
+
alias old_radio_button radio_button
|
39
|
+
}
|
40
|
+
|
41
|
+
SimpleMethods = \
|
42
|
+
['file_field', 'hidden_field', 'password_field', 'text_area', 'text_field']
|
43
|
+
|
44
|
+
public
|
45
|
+
|
46
|
+
once ('@simpleMethodsDone') { # because it calls alias.
|
47
|
+
SimpleMethods.each { | name | restrict(name) }
|
48
|
+
}
|
49
|
+
|
50
|
+
def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0")
|
51
|
+
restricted_input(:old_check_box, object, method, options, checked_value, unchecked_value) {
|
52
|
+
| object, method, options, extra |
|
53
|
+
old_check_box(object, method, options, extra[0], extra[1])
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
def radio_button(object, method, tag_value, options = {})
|
58
|
+
restricted_input(:old_radio_button, object, method, options, tag_value) {
|
59
|
+
| object, method, options, extra |
|
60
|
+
old_radio_button(object, method, extra[0], options)
|
61
|
+
}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|