model_security_generator 0.0.1
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/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
|
+
|