model_security_generator 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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' %>
|