model_security_generator 0.0.3 → 0.0.5
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/model_security_generator.rb +9 -11
- data/templates/ADD_TO_APPLICATION_CONTROLLER +13 -0
- data/templates/README +2 -0
- data/templates/RUN_DEMO +95 -0
- data/templates/USAGE +2 -0
- data/templates/controllers/user_controller.rb +7 -51
- data/templates/helpers/model_security_helper.rb +117 -53
- data/templates/lib/model_security.rb +163 -171
- data/templates/lib/user_support.rb +6 -0
- data/templates/mailer/forgot_password.rhtml +10 -16
- data/templates/mailer/new_user.rhtml +1 -1
- data/templates/models/user.rb +16 -3
- data/templates/views/forgot_password.rhtml +18 -0
- metadata +6 -7
- data/templates/views/_form.rhtml +0 -27
- data/templates/views/edit.rhtml +0 -10
- data/templates/views/list.rhtml +0 -35
- data/templates/views/show.rhtml +0 -10
data/model_security_generator.rb
CHANGED
@@ -43,26 +43,24 @@ class ModelSecurityGenerator < Rails::Generator::NamedBase
|
|
43
43
|
m.file "views/#{action}.rhtml", "app/views/user/#{action}.rhtml"
|
44
44
|
end
|
45
45
|
|
46
|
-
# Partials
|
47
|
-
m.directory "app/views/user"
|
48
|
-
partial_views.each do |action|
|
49
|
-
m.file "views/_#{action}.rhtml", "app/views/user/_#{action}.rhtml"
|
50
|
-
end
|
51
|
-
|
52
46
|
# Mailer
|
53
47
|
m.directory "app/views/user_mailer"
|
54
48
|
mailer_views.each do |action|
|
55
49
|
m.file "mailer/#{action}.rhtml", "app/views/user_mailer/#{action}.rhtml"
|
56
50
|
end
|
51
|
+
|
52
|
+
# Documentation
|
53
|
+
m.directory "manuals/ModelSecurity"
|
54
|
+
m.file "README", "manuals/ModelSecurity/README"
|
55
|
+
m.file "USAGE", "manuals/ModelSecurity/USAGE"
|
56
|
+
m.file "RUN_DEMO", "manuals/ModelSecurity/RUN_DEMO"
|
57
|
+
m.file "ADD_TO_APPLICATION_CONTROLLER",
|
58
|
+
"app/controllers/ADD_TO_APPLICATION_CONTROLLER.ModelSecurity"
|
57
59
|
end
|
58
60
|
end
|
59
61
|
|
60
62
|
def user_views
|
61
|
-
%w(activate admin_created created
|
62
|
-
end
|
63
|
-
|
64
|
-
def partial_views
|
65
|
-
%w(form)
|
63
|
+
%w(activate admin_created created forgot_password forgot_password_done login login_admin logout new success)
|
66
64
|
end
|
67
65
|
|
68
66
|
def mailer_views
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# The filters added to this controller will be run for all controllers in the application.
|
2
|
+
# Likewise will all the methods added be available for all controllers.
|
3
|
+
#
|
4
|
+
# Added Facilities for User model (and thus also for Modal and ModelSecurity).
|
5
|
+
#
|
6
|
+
require 'user_support'
|
7
|
+
|
8
|
+
class ApplicationController < ActionController::Base
|
9
|
+
helper :ModelSecurity
|
10
|
+
include UserSupport
|
11
|
+
|
12
|
+
before_filter :user_setup
|
13
|
+
end
|
data/templates/README
CHANGED
data/templates/RUN_DEMO
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
= Running the Model Security Demo =
|
2
|
+
|
3
|
+
Here's how to run the model security demo. Once you've gone through this
|
4
|
+
process, you should be able
|
5
|
+
|
6
|
+
Generate a rails program called <i>test</i>, and change to its directory:
|
7
|
+
|
8
|
+
rails test
|
9
|
+
cd test
|
10
|
+
|
11
|
+
Install the model_security gem:
|
12
|
+
|
13
|
+
gem install model_security -
|
14
|
+
|
15
|
+
Have MySQL execute the script to create the database and a MySQL user ID for the rails program:
|
16
|
+
|
17
|
+
cd db/
|
18
|
+
mysql -u <i>MYSQL-ADMIN-NAME</i> -p < demo.sql
|
19
|
+
|
20
|
+
Examine demo.sql and users.sql . Note that demo.sql runs users.sql to define
|
21
|
+
the users table. When you include ModelSecurity in another Rails program,
|
22
|
+
you'll skip demo.sql and use users.sql to create the users table.
|
23
|
+
|
24
|
+
Edit the database configuration in db/database.yml to look like this:
|
25
|
+
|
26
|
+
development:
|
27
|
+
adapter: mysql
|
28
|
+
database: model_security_demo
|
29
|
+
host: localhost
|
30
|
+
username: m_s_demo
|
31
|
+
password:
|
32
|
+
|
33
|
+
Add some necessary facilities to your brand new application controller:
|
34
|
+
|
35
|
+
cd ../app/controllers
|
36
|
+
cp ADD_TO_APPLICATION_CONTROLLER.ModelSecurity application.rb
|
37
|
+
cd ../..
|
38
|
+
|
39
|
+
If this wasn't a new controller, you would edit application.rb to add the
|
40
|
+
facilities requested, rather than copying over the file.
|
41
|
+
|
42
|
+
Start the rails server:
|
43
|
+
|
44
|
+
script/server &
|
45
|
+
|
46
|
+
Open a web browser to http://localhost:3000/user/new
|
47
|
+
Use the form to create a new user. That user will automatically be
|
48
|
+
granted the administrator role, and will be activated immediately.
|
49
|
+
|
50
|
+
Subsequent users will <i>not</i> automatically get the administrator role,
|
51
|
+
although an administrator can grant it to them by editing their record
|
52
|
+
with /user/edit/<i>user-name</i> . Subsequent users will
|
53
|
+
not have their login activated immediately when they create it. Instead,
|
54
|
+
they will be sent an email containing a URL that they must use to activate
|
55
|
+
their login.
|
56
|
+
|
57
|
+
Now, you can play with the demo. The user controller responds to:
|
58
|
+
|
59
|
+
* /user/activate/<i>user-name</i>?token=<i>security-token</i>
|
60
|
+
Activate a user, after the user's login has been created.
|
61
|
+
|
62
|
+
* /user/destroy/<i>user-name</i>
|
63
|
+
Destroy a user. Requires the administrator role.
|
64
|
+
|
65
|
+
* /user/edit
|
66
|
+
Edit the attributes of a user. Administrators are allowed to edit more
|
67
|
+
fields than normal users, and normal are allowed to edit their own
|
68
|
+
record, not anyone else's.
|
69
|
+
|
70
|
+
* /user/forgot_password
|
71
|
+
Send an email to the user that facilitates password recovery.
|
72
|
+
|
73
|
+
* /user/list
|
74
|
+
List the users. Administrators can see more information than normal
|
75
|
+
users. Normal users can see some information on their own record
|
76
|
+
that they will not see in the records of other users.
|
77
|
+
|
78
|
+
* /user/login
|
79
|
+
Perform HTTP authentication to log in a user. If that doesn't work,
|
80
|
+
fall back on a login form.
|
81
|
+
|
82
|
+
* /user/logout
|
83
|
+
Log a user out. Actually loops back to the login action, because the only
|
84
|
+
way to get the browser to stop sending HTTP authentication data with
|
85
|
+
every request is to ask it to get new authentication data from the
|
86
|
+
user.
|
87
|
+
|
88
|
+
* /user/new
|
89
|
+
Create a new user.
|
90
|
+
|
91
|
+
* /user/show
|
92
|
+
Display information about a user. Administrators can see more information
|
93
|
+
than normal users.
|
94
|
+
Normal users can see some information on their own record
|
95
|
+
that they will not see in the records of other users.
|
data/templates/USAGE
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
+
# Manipulate the User model.
|
2
|
+
#
|
3
|
+
# This (and the User model) should probably be merged with ActiveRBAC.
|
4
|
+
#
|
1
5
|
# The HTTP authorization code is
|
2
6
|
# derived from an example published by Maximillian Dornseif at
|
3
7
|
# http://blogs.23.nu/c0re/stories/7409/
|
@@ -9,7 +13,8 @@
|
|
9
13
|
# say that this is derived from the work of Joe Hosteny and Tobias Leutke.
|
10
14
|
#
|
11
15
|
class UserController < ApplicationController
|
12
|
-
|
16
|
+
scaffold :user
|
17
|
+
helper :ModelSecurity
|
13
18
|
|
14
19
|
private
|
15
20
|
# Require_admin will require an administrative login before the action
|
@@ -31,13 +36,6 @@ private
|
|
31
36
|
render :status => 401
|
32
37
|
end
|
33
38
|
|
34
|
-
def initialize
|
35
|
-
end
|
36
|
-
|
37
|
-
public
|
38
|
-
scaffold :user
|
39
|
-
|
40
|
-
private
|
41
39
|
public
|
42
40
|
|
43
41
|
# Activate a new user, having logged in with a security token. All of the
|
@@ -45,28 +43,6 @@ public
|
|
45
43
|
def activate
|
46
44
|
end
|
47
45
|
|
48
|
-
# Destroy a user object.
|
49
|
-
def destroy
|
50
|
-
user = User.find(@params[:id])
|
51
|
-
user.destroy
|
52
|
-
flash['notice'] = 'User destroyed.'
|
53
|
-
redirect_to :action => :list
|
54
|
-
end
|
55
|
-
|
56
|
-
# Edit a user. Will only allow you to edit what your security clearance
|
57
|
-
# allows, due to the magic of model security.
|
58
|
-
def edit
|
59
|
-
case @request.method
|
60
|
-
when :get
|
61
|
-
@user = User.find(@params[:id])
|
62
|
-
when :post
|
63
|
-
@user = User.find(@params[:id])
|
64
|
-
@user.attributes = @params['user']
|
65
|
-
@user.save
|
66
|
-
redirect_to :action => :list
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
46
|
# Send out a forgot-password email.
|
71
47
|
def forgot_password
|
72
48
|
case @request.method
|
@@ -89,19 +65,6 @@ public
|
|
89
65
|
def forgot_password_done
|
90
66
|
end
|
91
67
|
|
92
|
-
# List users.
|
93
|
-
# FIX: Get away from the scaffold here, and use the login instead of the ID
|
94
|
-
# to access a user record. Using an ID gives outsiders a way to enumerate
|
95
|
-
# the users, which has security implications.
|
96
|
-
def list
|
97
|
-
options = {
|
98
|
-
:per_page => 10,
|
99
|
-
:order_by => 'name, id'
|
100
|
-
}
|
101
|
-
|
102
|
-
@user_pages, @users = paginate(:user, options)
|
103
|
-
end
|
104
|
-
|
105
68
|
# Attempt HTTP authentication, and fall back on a login form.
|
106
69
|
# If this method is called login_admin (it's an alias), keep trying
|
107
70
|
# until an administrator logs in or the user pushes the "back" button.
|
@@ -150,6 +113,7 @@ public
|
|
150
113
|
@user.activated = 1
|
151
114
|
@user.save
|
152
115
|
User.sign_on_by_session(1)
|
116
|
+
session[:user_id] = 1
|
153
117
|
render :file => 'app/views/user/admin_created.rhtml'
|
154
118
|
# Mail the user instructions on how to activate their account.
|
155
119
|
else
|
@@ -169,14 +133,6 @@ public
|
|
169
133
|
end
|
170
134
|
end
|
171
135
|
|
172
|
-
# Display a user's information.
|
173
|
-
# FIX: Use the user's login instead of the record ID.
|
174
|
-
# Using the record ID provides an easy way for outsiders to enumerate the
|
175
|
-
# users, which has security implications.
|
176
|
-
def show
|
177
|
-
@user = User.find(@params[:id])
|
178
|
-
end
|
179
|
-
|
180
136
|
# Tell the user that an action succeeded.
|
181
137
|
def success
|
182
138
|
end
|
@@ -1,64 +1,128 @@
|
|
1
|
-
|
2
|
-
private
|
1
|
+
require 'once'
|
3
2
|
|
4
|
-
|
5
|
-
|
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
|
3
|
+
module ModelSecurityHelper
|
4
|
+
end
|
12
5
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
6
|
+
module ActiveRecord
|
7
|
+
class Base
|
8
|
+
once('@instanceAliasesDone') {
|
9
|
+
# Provides access to the pre-overload version of read_attribute, which
|
10
|
+
# is called by the overloaded version.
|
11
|
+
alias :old_read_attribute :read_attribute
|
12
|
+
|
13
|
+
# Provides access to the pre-overload version of write_attribute, which
|
14
|
+
# is called by the overloaded version.
|
15
|
+
alias :old_write_attribute :write_attribute
|
16
|
+
}
|
17
|
+
|
18
|
+
class << self
|
19
|
+
private
|
20
|
+
once('@classAliasesDone') {
|
21
|
+
# Provides access to the pre-overload version of content_columns, which
|
22
|
+
# is called by the overloaded version.
|
23
|
+
alias :old_content_columns :content_columns
|
24
|
+
}
|
25
|
+
public
|
26
|
+
# Overload the base method to understand the let_display directive
|
27
|
+
# of ModelSecurity. If display? is not true for a model attribute
|
28
|
+
# in this context, that attribute won't be reported as a content
|
29
|
+
# column.
|
30
|
+
def content_columns
|
31
|
+
old_content_columns.reject { |c|
|
32
|
+
not display?(c.name)
|
33
|
+
}
|
32
34
|
end
|
33
35
|
end
|
34
36
|
end
|
37
|
+
end
|
35
38
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
module ActionView
|
40
|
+
module Helpers
|
41
|
+
# Overload the form helper functions to understand ModelSecurity and
|
42
|
+
# act upon the permissions that ModelSecurity encodes. If a model
|
43
|
+
# attribute does not have a read permission in the current context,
|
44
|
+
# its data won't be displayed, or read at all, and thus the helpers
|
45
|
+
# won't trigger a ModelSecurity exception. If there's no write
|
46
|
+
# permission on the datum, you'll display static data instead of an
|
47
|
+
# edit field. Write-only attributes work too. And all of this works
|
48
|
+
# on scaffolds.
|
49
|
+
#
|
50
|
+
# The methods overriden here are:
|
51
|
+
#
|
52
|
+
# * check_box
|
53
|
+
# * file_field
|
54
|
+
# * hidden_field
|
55
|
+
# * password_field
|
56
|
+
# * radio_button
|
57
|
+
# * text_area
|
58
|
+
# * text_field
|
59
|
+
#
|
60
|
+
module FormHelper
|
61
|
+
private
|
62
|
+
|
63
|
+
def self.restrict(name)
|
64
|
+
module_eval <<-"end_eval", __FILE__, __LINE__
|
65
|
+
private
|
66
|
+
alias :old_#{name} :#{name}
|
67
|
+
def #{name}(object, method, options = {})
|
68
|
+
restricted_input('#{name}', object, method, options)
|
69
|
+
end
|
70
|
+
end_eval
|
71
|
+
end
|
72
|
+
|
73
|
+
def restricted_input(function, object, method, options, *extra, &block)
|
74
|
+
o = instance_variable_get('@' + object)
|
45
75
|
|
46
|
-
|
47
|
-
|
48
|
-
|
76
|
+
if o.readable?(method)
|
77
|
+
if o.writable?(method) # Read+Write
|
78
|
+
if block
|
79
|
+
return yield(object, method, options, extra)
|
80
|
+
else
|
81
|
+
return send(('old_' + function).to_sym, object, method, options)
|
82
|
+
end
|
83
|
+
else # Read Only
|
84
|
+
return o.send(method)
|
85
|
+
end
|
86
|
+
else # Not Readable.
|
87
|
+
if o.writable?(method) # Write only.
|
88
|
+
u = { :value => '' }
|
49
89
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
90
|
+
return send(('old_' + function).to_sym, object, method, options.update(u))
|
91
|
+
else # Neither readable or writable.
|
92
|
+
return ''
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Evaluating the aliases twice would break them.
|
98
|
+
# Evaluating the constant definition twice causes a complaint.
|
99
|
+
once ('@aliasesDone') {
|
100
|
+
alias :old_check_box :check_box
|
101
|
+
alias :old_radio_button :radio_button
|
102
|
+
}
|
103
|
+
|
104
|
+
|
105
|
+
public
|
106
|
+
|
107
|
+
once ('@simpleMethodsDone') { # because it calls alias.
|
108
|
+
m = %w(file_field hidden_field password_field text_area text_field)
|
56
109
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
110
|
+
m.each { | name | restrict(name) }
|
111
|
+
}
|
112
|
+
|
113
|
+
def check_box(object, method, options = {}, checked_value = "1", unchecked_value = "0") #:nodoc:
|
114
|
+
restricted_input('check_box', object, method, options, checked_value, unchecked_value) {
|
115
|
+
| object, method, options, extra |
|
116
|
+
old_check_box(object, method, options, extra[0], extra[1])
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def radio_button(object, method, tag_value, options = {}) #:nodoc:
|
121
|
+
restricted_input('radio_button', object, method, options, tag_value) {
|
122
|
+
| object, method, options, extra |
|
123
|
+
old_radio_button(object, method, extra[0], options)
|
124
|
+
}
|
125
|
+
end
|
126
|
+
end
|
62
127
|
end
|
63
128
|
end
|
64
|
-
|
@@ -1,3 +1,165 @@
|
|
1
|
+
require 'once'
|
2
|
+
|
3
|
+
module ModelSecurity
|
4
|
+
private
|
5
|
+
# Functions that are both class and instance methods within ModelSecurity.
|
6
|
+
module BothClassAndInstanceMethods
|
7
|
+
private
|
8
|
+
# Run a single test.
|
9
|
+
def run_test t
|
10
|
+
case t.class.name
|
11
|
+
when 'Proc'
|
12
|
+
return t.call(binding)
|
13
|
+
when 'Symbol'
|
14
|
+
return self.send(t)
|
15
|
+
when 'String'
|
16
|
+
return eval(t)
|
17
|
+
else
|
18
|
+
return false
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# This does the permission test for readable?, writable?, and display?.
|
23
|
+
# A global variable is used to short-circuit recursion.
|
24
|
+
#
|
25
|
+
# FIX: The global variable should be replaced with a thread-local variable
|
26
|
+
# once I learn how to make one.
|
27
|
+
#
|
28
|
+
def run_tests(d, attribute)
|
29
|
+
global = d[:all]
|
30
|
+
local = d[attribute.to_sym]
|
31
|
+
result = true
|
32
|
+
|
33
|
+
if (global or local) and ($in_test_permission != true)
|
34
|
+
$in_test_permission = true
|
35
|
+
result = (run_test(global) or run_test(local))
|
36
|
+
$in_test_permission = false
|
37
|
+
end
|
38
|
+
return result
|
39
|
+
end
|
40
|
+
|
41
|
+
public
|
42
|
+
# Stub test for let_read and friends. Always returns true.
|
43
|
+
def always?
|
44
|
+
true
|
45
|
+
end
|
46
|
+
|
47
|
+
# Stub test for let_read and friends. Always returns false.
|
48
|
+
def never?
|
49
|
+
false
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Class methods for ModelSecurity. They are broken out this way so that they
|
55
|
+
# can be fed to base.extend(), Ruby interpreter magic so that class methods
|
56
|
+
# of this module work while a class including it is being defined.
|
57
|
+
# Uses a Rails-internal inheritable-attribute mechanism so that this data in a
|
58
|
+
# derived class survives modification of similar data in its base class.
|
59
|
+
#
|
60
|
+
module ModelSecurity
|
61
|
+
private
|
62
|
+
module ClassMethods
|
63
|
+
private
|
64
|
+
include BothClassAndInstanceMethods
|
65
|
+
|
66
|
+
# Internal function where the work of let_read, let_write, let_access,
|
67
|
+
# and let_display is done. Store the tests to be run for each attribute
|
68
|
+
# in the class, to be evaluated later. *permission* is :let_read,
|
69
|
+
# :let_write, or :let_display. *arguments* is a list of attributes
|
70
|
+
# upon which security permissions are being declared and a hash
|
71
|
+
# containing all options, currently just :if . *block*, if given,
|
72
|
+
# contains a security test.
|
73
|
+
#
|
74
|
+
def let(permission, arguments, block)
|
75
|
+
attributes = [] # List of attributes that this permission applies to.
|
76
|
+
parameters = {} # Optional parameters, currently only :if
|
77
|
+
procedure = nil # Permission-test procedure.
|
78
|
+
|
79
|
+
arguments.each { | argument |
|
80
|
+
case argument.class.name
|
81
|
+
when 'Hash'
|
82
|
+
parameters.merge! argument
|
83
|
+
else
|
84
|
+
attributes << argument
|
85
|
+
end
|
86
|
+
}
|
87
|
+
if not block.nil?
|
88
|
+
procedure = block
|
89
|
+
elsif (p = parameters[:if])
|
90
|
+
procedure = p
|
91
|
+
else
|
92
|
+
procedure = :always?
|
93
|
+
end
|
94
|
+
|
95
|
+
d = {}
|
96
|
+
attributes.each { | name | d[name] = procedure }
|
97
|
+
write_inheritable_hash(permission, d)
|
98
|
+
end
|
99
|
+
|
100
|
+
public
|
101
|
+
|
102
|
+
# Install default permissions for all of the attributes that Rails defines.
|
103
|
+
#
|
104
|
+
# Readable:
|
105
|
+
# created_at, created_on, type, id, updated_at, updated_on,
|
106
|
+
# lock_version, position, parent_id, lft, rgt,
|
107
|
+
# table_name + '_count'
|
108
|
+
#
|
109
|
+
# Writable:
|
110
|
+
# updated_at, updated_on, lock_version, position, parent_id, lft, rgt
|
111
|
+
#
|
112
|
+
# Writable only before the first save of an Active Record:
|
113
|
+
# created_at, created_on, type, id
|
114
|
+
#
|
115
|
+
def default_permissions
|
116
|
+
let_read :created_at, :created_on, :type, :id, :updated_at, \
|
117
|
+
:updated_on, :lock_version, :position, :parent_id, :lft, :rgt, \
|
118
|
+
(table_name + '_count').to_sym
|
119
|
+
|
120
|
+
# These shouldn't change after the first save.
|
121
|
+
let_write :created_at, :created_on, :type, :id, :if => :new_record?
|
122
|
+
|
123
|
+
# These can change.
|
124
|
+
let_write :updated_at, :updated_on, :lock_version, :position, :parent_id, \
|
125
|
+
:lft, :rgt
|
126
|
+
end
|
127
|
+
|
128
|
+
# Return true if display of the attribute is permitted. *attribute* is a
|
129
|
+
# symbol, and should match a field in the database schema corresponding to
|
130
|
+
# this model.
|
131
|
+
def display?(attribute)
|
132
|
+
if (d = read_inheritable_attribute(:let_display))
|
133
|
+
return run_tests(d, attribute)
|
134
|
+
else
|
135
|
+
return true
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# Declare whether reads and writes are permitted on the named attributes.
|
140
|
+
def let_access(*arguments, &block)
|
141
|
+
let(:let_read, arguments, block)
|
142
|
+
let(:let_write, arguments, block)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Declare whether display of the named attribute is permitted.
|
146
|
+
def let_display(*arguments, &block)
|
147
|
+
let(:let_display, arguments, block)
|
148
|
+
end
|
149
|
+
|
150
|
+
|
151
|
+
# Declare whether read is permitted upon the named attributes.
|
152
|
+
def let_read(*arguments, &block)
|
153
|
+
let(:let_read, arguments, block)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Declare whether write is permitted upon the named attributes.
|
157
|
+
def let_write(*arguments, &block)
|
158
|
+
let(:let_write, arguments, block)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
1
163
|
# The ModelSecurity module allows you to specify security permissions on any
|
2
164
|
# or all of the attributes of a model implemented using ActiveRecord.
|
3
165
|
#
|
@@ -99,59 +261,7 @@
|
|
99
261
|
# access is attempted.
|
100
262
|
#
|
101
263
|
module ModelSecurity
|
102
|
-
|
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
|
264
|
+
include BothClassAndInstanceMethods
|
155
265
|
|
156
266
|
# This does the permission test for readable? or writable?.
|
157
267
|
def test_permission(permission, attribute)
|
@@ -241,121 +351,3 @@ public
|
|
241
351
|
end
|
242
352
|
end
|
243
353
|
|
244
|
-
# Class methods for ModelSecurity. They are broken out this way so that they
|
245
|
-
# can be fed to base.extend(), Ruby interpreter magic so that class methods
|
246
|
-
# of this module work while a class including it is being defined.
|
247
|
-
# Uses a Rails-internal inheritable-attribute mechanism so that this data in a
|
248
|
-
# derived class survives modification of similar data in its base class.
|
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
|
-
|
290
|
-
# Install default permissions for all of the attributes that Rails defines.
|
291
|
-
#
|
292
|
-
# Readable:
|
293
|
-
# created_at, created_on, type, id, updated_at, updated_on,
|
294
|
-
# lock_version, position, parent_id, lft, rgt,
|
295
|
-
# table_name + '_count'
|
296
|
-
#
|
297
|
-
# Writable:
|
298
|
-
# updated_at, updated_on, lock_version, position, parent_id, lft, rgt
|
299
|
-
#
|
300
|
-
# Writable only before the first save of an Active Record:
|
301
|
-
# created_at, created_on, type, id
|
302
|
-
#
|
303
|
-
def default_permissions
|
304
|
-
let_read :created_at, :created_on, :type, :id, :updated_at, \
|
305
|
-
:updated_on, :lock_version, :position, :parent_id, :lft, :rgt, \
|
306
|
-
(table_name + '_count').to_sym
|
307
|
-
|
308
|
-
# These shouldn't change after the first save.
|
309
|
-
let_write :created_at, :created_on, :type, :id, :if => :new_record?
|
310
|
-
|
311
|
-
# These can change.
|
312
|
-
let_write :updated_at, :updated_on, :lock_version, :position, :parent_id, \
|
313
|
-
:lft, :rgt
|
314
|
-
end
|
315
|
-
|
316
|
-
# Return true if display of the attribute is permitted. *attribute* is a
|
317
|
-
# symbol, and should match a field in the database schema corresponding to
|
318
|
-
# this model.
|
319
|
-
def display?(attribute)
|
320
|
-
if (d = read_inheritable_attribute(:let_display))
|
321
|
-
return run_tests(d, attribute)
|
322
|
-
else
|
323
|
-
return true
|
324
|
-
end
|
325
|
-
end
|
326
|
-
|
327
|
-
# Declare whether reads and writes are permitted on the named attributes.
|
328
|
-
def let_access(*arguments, &block)
|
329
|
-
let(:let_read, arguments, block)
|
330
|
-
let(:let_write, arguments, block)
|
331
|
-
end
|
332
|
-
|
333
|
-
# Declare whether display of the named attribute is permitted.
|
334
|
-
def let_display(*arguments, &block)
|
335
|
-
let(:let_display, arguments, block)
|
336
|
-
end
|
337
|
-
|
338
|
-
|
339
|
-
# Declare whether read is permitted upon the named attributes.
|
340
|
-
def let_read(*arguments, &block)
|
341
|
-
let(:let_read, arguments, block)
|
342
|
-
end
|
343
|
-
|
344
|
-
# Declare whether write is permitted upon the named attributes.
|
345
|
-
def let_write(*arguments, &block)
|
346
|
-
let(:let_write, arguments, block)
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
class ActiveRecord::Base
|
351
|
-
private
|
352
|
-
once ('@aliasesDone') {
|
353
|
-
# Provides access to the pre-overload version of read_attribute, which
|
354
|
-
# is called by the overloaded version.
|
355
|
-
alias old_read_attribute read_attribute
|
356
|
-
|
357
|
-
# Provides access to the pre-overload version of write_attribute, which
|
358
|
-
# is called by the overloaded version.
|
359
|
-
alias old_write_attribute write_attribute
|
360
|
-
}
|
361
|
-
end
|
@@ -14,6 +14,8 @@ require 'user'
|
|
14
14
|
require 'modal'
|
15
15
|
|
16
16
|
module UserSupport
|
17
|
+
include Modal
|
18
|
+
|
17
19
|
# Return true if the currently-logged-in user is the administrator.
|
18
20
|
def admin?
|
19
21
|
User.admin?
|
@@ -62,6 +64,10 @@ module UserSupport
|
|
62
64
|
# that expects login information.
|
63
65
|
#
|
64
66
|
def user_setup
|
67
|
+
# require_* use Modal to return to what they were doing after HTTP
|
68
|
+
# authentication.
|
69
|
+
modal_setup
|
70
|
+
|
65
71
|
# This is used by the logout action to discard the old HTTP authentiction.
|
66
72
|
# Logout redirects to login and that generates a new authentication
|
67
73
|
# request. That request is the only input that can tell the browser to
|
@@ -1,18 +1,12 @@
|
|
1
|
-
|
1
|
+
Dear <%= @params['name'] %>,
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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 %>
|
3
|
+
To reset the password for your login
|
4
|
+
'<%= @params['login'] %>'
|
5
|
+
please access the following URL before
|
6
|
+
<%= @token_expiry %>:
|
17
7
|
|
18
|
-
<%=
|
8
|
+
<%= @url %>
|
9
|
+
|
10
|
+
Many Thanks
|
11
|
+
|
12
|
+
The Management
|
data/templates/models/user.rb
CHANGED
@@ -92,6 +92,7 @@ private
|
|
92
92
|
|
93
93
|
# These just control display.
|
94
94
|
let_display :all, :if => :never?
|
95
|
+
let_display :admin, :activated, :if => :admin?
|
95
96
|
let_display :login, :name, :email
|
96
97
|
|
97
98
|
# These control both reading and writing.
|
@@ -115,7 +116,7 @@ private
|
|
115
116
|
# Allow the very first user to be promoted to administrator.
|
116
117
|
# Once there's an admin, that user has "let_access :all" and can
|
117
118
|
# promote others to administrator.
|
118
|
-
let_write :admin, :if => :initial_self_promotion
|
119
|
+
let_write :admin, :if => :initial_self_promotion?
|
119
120
|
|
120
121
|
# If this is a new (never saved) record, or if this record corresponds to
|
121
122
|
# the currently-logged-in user, allow reading of the email address.
|
@@ -137,12 +138,24 @@ private
|
|
137
138
|
# unless this record is new (has never been saved).
|
138
139
|
let_write :cypher, :email, :salt, :if => :new_or_me?
|
139
140
|
|
141
|
+
|
140
142
|
# The security token can only be changed if we're the special "login" user.
|
141
|
-
let_write :activated, :token, :token_expiry, :if => :logging_in?
|
143
|
+
let_write :activated, :password, :token, :token_expiry, :if => :logging_in?
|
142
144
|
|
143
145
|
public
|
144
146
|
attr_accessor :password, :password_confirmation, :old_password
|
145
147
|
|
148
|
+
# NOTE: :password, :password_confirmation, and :old_password
|
149
|
+
# are not attributes of the record, they are instance variables of the
|
150
|
+
# class and aren't written to disk under those names. But I declare them
|
151
|
+
# here because otherwise ModelSecurityHelper (which doesn't know that)
|
152
|
+
# isn't going to allow me to enter them into a form field.
|
153
|
+
#
|
154
|
+
# I like how fine-grained I can get.
|
155
|
+
let_write :password, :if => :new_or_me_or_logging_in?
|
156
|
+
let_write :password_confirmation, :if => :new_or_me?
|
157
|
+
let_write :old_password, :if => :me?
|
158
|
+
|
146
159
|
# Change the user's password. Confirm the old password while doing so.
|
147
160
|
def change_password(attributes)
|
148
161
|
@password_is_new = true
|
@@ -191,7 +204,7 @@ public
|
|
191
204
|
# himself to administrator. This is used to bootstrap the first administrator
|
192
205
|
# and for no other purpose.
|
193
206
|
def initial_self_promotion?
|
194
|
-
return (
|
207
|
+
return ((id == 1) and (not admin?) and (self.class.count == 1))
|
195
208
|
end
|
196
209
|
|
197
210
|
# Return true if the user is currently logging in. This security test allows
|
@@ -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' %>
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: model_security_generator
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.0.
|
7
|
-
date: 2005-
|
6
|
+
version: 0.0.5
|
7
|
+
date: 2005-10-04
|
8
8
|
summary: "[Rails] Model security and authentication generator."
|
9
9
|
require_paths:
|
10
10
|
- "."
|
@@ -15,7 +15,7 @@ description: Generates Rails code implementing a model security and authenticati
|
|
15
15
|
autorequire:
|
16
16
|
default_executable:
|
17
17
|
bindir: bin
|
18
|
-
has_rdoc:
|
18
|
+
has_rdoc: true
|
19
19
|
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
20
|
requirements:
|
21
21
|
-
|
@@ -31,6 +31,8 @@ files:
|
|
31
31
|
- model_security_generator.rb
|
32
32
|
- templates/README
|
33
33
|
- templates/USAGE
|
34
|
+
- templates/RUN_DEMO
|
35
|
+
- templates/ADD_TO_APPLICATION_CONTROLLER
|
34
36
|
- templates/controllers/user_controller.rb
|
35
37
|
- templates/db/demo.sql
|
36
38
|
- templates/db/users.sql
|
@@ -49,18 +51,15 @@ files:
|
|
49
51
|
- templates/test/user_controller_test.rb
|
50
52
|
- templates/test/user_test.rb
|
51
53
|
- templates/test/users.yml
|
52
|
-
- templates/views/_form.rhtml
|
53
54
|
- templates/views/activate.rhtml
|
54
55
|
- templates/views/admin_created.rhtml
|
55
56
|
- templates/views/created.rhtml
|
56
|
-
- templates/views/
|
57
|
+
- templates/views/forgot_password.rhtml
|
57
58
|
- templates/views/forgot_password_done.rhtml
|
58
|
-
- templates/views/list.rhtml
|
59
59
|
- templates/views/login.rhtml
|
60
60
|
- templates/views/login_admin.rhtml
|
61
61
|
- templates/views/logout.rhtml
|
62
62
|
- templates/views/new.rhtml
|
63
|
-
- templates/views/show.rhtml
|
64
63
|
- templates/views/success.rhtml
|
65
64
|
test_files: []
|
66
65
|
rdoc_options: []
|
data/templates/views/_form.rhtml
DELETED
@@ -1,27 +0,0 @@
|
|
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
|
-
|
data/templates/views/edit.rhtml
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
<h1>Editing user</h1>
|
2
|
-
|
3
|
-
<% error_messages_for 'user' %>
|
4
|
-
<%= start_form_tag :action => 'edit', :id => @user %>
|
5
|
-
<%= render_partial 'form' %>
|
6
|
-
<%= submit_tag 'Edit' %>
|
7
|
-
<%= end_form_tag %>
|
8
|
-
|
9
|
-
<%= link_to 'Show', :action => 'show', :id => @user %> |
|
10
|
-
<%= link_to 'Back', :action => 'list' %>
|
data/templates/views/list.rhtml
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
<h1>Listing users</h1>
|
2
|
-
|
3
|
-
<table>
|
4
|
-
<tr>
|
5
|
-
<% for column in User.content_columns %>
|
6
|
-
<% if User.display?(column.name) %>
|
7
|
-
<th><%= column.human_name %></th>
|
8
|
-
<% end %>
|
9
|
-
<% end %>
|
10
|
-
</tr>
|
11
|
-
|
12
|
-
<% for user in @users %>
|
13
|
-
<tr>
|
14
|
-
<% for column in User.content_columns %>
|
15
|
-
<% if User.display?(column.name) %>
|
16
|
-
<% if user.readable?(column.name) %>
|
17
|
-
<td><%= user.send(column.name) %></td>
|
18
|
-
<% else %>
|
19
|
-
<td></td>
|
20
|
-
<% end %>
|
21
|
-
<% end %>
|
22
|
-
<% end %>
|
23
|
-
<td><%= link_to 'Show', :action => 'show', :id => user %></td>
|
24
|
-
<td><%= link_to 'Edit', :action => 'edit', :id => user %></td>
|
25
|
-
<td><%= link_to 'Destroy', {:action => 'destroy', :id => user}, :confirm => 'Are you sure?' %></td>
|
26
|
-
</tr>
|
27
|
-
<% end %>
|
28
|
-
</table>
|
29
|
-
|
30
|
-
<%= link_to 'Previous page', { :page => @user_pages.current.previous } if @user_pages.current.previous %>
|
31
|
-
<%= link_to 'Next page', { :page => @user_pages.current.next } if @user_pages.current.next %>
|
32
|
-
|
33
|
-
<br />
|
34
|
-
|
35
|
-
<%= link_to 'New user', :action => 'new' %>
|
data/templates/views/show.rhtml
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
<% for column in User.content_columns %>
|
2
|
-
<% if User.display?(column.name) and @user.readable?(column.name) %>
|
3
|
-
<p>
|
4
|
-
<b><%= column.human_name %>:</b> <%=h @user.send(column.name) %>
|
5
|
-
</p>
|
6
|
-
<% end %>
|
7
|
-
<% end %>
|
8
|
-
|
9
|
-
<%= link_to 'Edit', :action => 'edit', :id => @user %> |
|
10
|
-
<%= link_to 'Back', :action => 'list' %>
|