model_security_generator 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +0 -0
- data/USAGE +0 -0
- data/model_security_generator.rb +75 -0
- data/templates/_view_form.rhtml +27 -0
- data/templates/mailer_forgot_password.rhtml +18 -0
- data/templates/mailer_new_user.rhtml +10 -0
- data/templates/mock_mailer.rb +16 -0
- data/templates/mock_time.rb +17 -0
- data/templates/modal.rb +82 -0
- data/templates/modal_helper.rb +29 -0
- data/templates/model_security.rb +334 -0
- data/templates/model_security_helper.rb +64 -0
- data/templates/once.rb +36 -0
- data/templates/scaffold.css +74 -0
- data/templates/scaffold.rhtml +11 -0
- data/templates/schema.sql +4 -0
- data/templates/standard.css +7 -0
- data/templates/standard.rhtml +16 -0
- data/templates/user.rb +328 -0
- data/templates/user_controller.rb +178 -0
- data/templates/user_controller_test.rb +20 -0
- data/templates/user_mailer.rb +29 -0
- data/templates/user_support.rb +124 -0
- data/templates/user_test.rb +10 -0
- data/templates/users.sql +17 -0
- data/templates/users.yml +41 -0
- data/templates/view_activate.rhtml +1 -0
- data/templates/view_edit.rhtml +10 -0
- data/templates/view_forgot_password_done.rhtml +5 -0
- data/templates/view_list.rhtml +35 -0
- data/templates/view_login.rhtml +11 -0
- data/templates/view_login_admin.rhtml +16 -0
- data/templates/view_logout.rhtml +1 -0
- data/templates/view_new.rhtml +30 -0
- data/templates/view_show.rhtml +10 -0
- data/templates/view_success.rhtml +1 -0
- metadata +83 -0
data/templates/once.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
# Do something only once. Called this way:
|
2
|
+
#
|
3
|
+
# once('variable-name') { block }
|
4
|
+
#
|
5
|
+
# *variable-name* is a string. The corresponding variable is referenced
|
6
|
+
# within the block's binding using eval().
|
7
|
+
#
|
8
|
+
# If the variable doesn't exist, or if it's false or nil:
|
9
|
+
# The block is executed.
|
10
|
+
# The variable is set to true, within the block's binding.
|
11
|
+
# True is returned.
|
12
|
+
#
|
13
|
+
# If the variable is true:
|
14
|
+
#
|
15
|
+
# False is returned.
|
16
|
+
#
|
17
|
+
# Although this will by default create a simple boolean, more complex
|
18
|
+
# variables such as object.method calls, array and hash references work
|
19
|
+
# fine. I've used it effectively with 2-dimensional hashes.
|
20
|
+
#
|
21
|
+
# Copyright (C) 2005 Bruce Perens <bruce@perens.com>
|
22
|
+
# Distributable under the same license as Ruby.
|
23
|
+
def once var, &block
|
24
|
+
begin
|
25
|
+
result = eval(var, block.binding)
|
26
|
+
rescue NameError
|
27
|
+
result = false
|
28
|
+
end
|
29
|
+
if not result
|
30
|
+
eval(var + " = true", block.binding)
|
31
|
+
yield
|
32
|
+
true
|
33
|
+
else
|
34
|
+
false
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
body { background-color: #fff; color: #333; }
|
2
|
+
|
3
|
+
body, p, ol, ul, td {
|
4
|
+
font-family: verdana, arial, helvetica, sans-serif;
|
5
|
+
font-size: 13px;
|
6
|
+
line-height: 18px;
|
7
|
+
}
|
8
|
+
|
9
|
+
pre {
|
10
|
+
background-color: #eee;
|
11
|
+
padding: 10px;
|
12
|
+
font-size: 11px;
|
13
|
+
}
|
14
|
+
|
15
|
+
a { color: #000; }
|
16
|
+
a:visited { color: #666; }
|
17
|
+
a:hover { color: #fff; background-color:#000; }
|
18
|
+
|
19
|
+
.fieldWithErrors {
|
20
|
+
padding: 2px;
|
21
|
+
background-color: red;
|
22
|
+
display: table;
|
23
|
+
}
|
24
|
+
|
25
|
+
#ErrorExplanation {
|
26
|
+
width: 400px;
|
27
|
+
border: 2px solid 'red';
|
28
|
+
padding: 7px;
|
29
|
+
padding-bottom: 12px;
|
30
|
+
margin-bottom: 20px;
|
31
|
+
background-color: #f0f0f0;
|
32
|
+
}
|
33
|
+
|
34
|
+
#ErrorExplanation h2 {
|
35
|
+
text-align: left;
|
36
|
+
font-weight: bold;
|
37
|
+
padding: 5px 5px 5px 15px;
|
38
|
+
font-size: 12px;
|
39
|
+
margin: -7px;
|
40
|
+
background-color: #c00;
|
41
|
+
color: #fff;
|
42
|
+
}
|
43
|
+
|
44
|
+
#ErrorExplanation p {
|
45
|
+
color: #333;
|
46
|
+
margin-bottom: 0;
|
47
|
+
padding: 5px;
|
48
|
+
}
|
49
|
+
|
50
|
+
#ErrorExplanation ul li {
|
51
|
+
font-size: 12px;
|
52
|
+
list-style: square;
|
53
|
+
}
|
54
|
+
|
55
|
+
div.uploadStatus {
|
56
|
+
margin: 5px;
|
57
|
+
}
|
58
|
+
|
59
|
+
div.progressBar {
|
60
|
+
margin: 5px;
|
61
|
+
}
|
62
|
+
|
63
|
+
div.progressBar div.border {
|
64
|
+
background-color: #fff;
|
65
|
+
border: 1px solid grey;
|
66
|
+
width: 100%;
|
67
|
+
}
|
68
|
+
|
69
|
+
div.progressBar div.background {
|
70
|
+
background-color: #333;
|
71
|
+
height: 18px;
|
72
|
+
width: 0%;
|
73
|
+
}
|
74
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
2
|
+
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
3
|
+
<head>
|
4
|
+
<link href="/stylesheets/standard.css" rel="stylesheet" type="text/css" />
|
5
|
+
</head>
|
6
|
+
<body class="page">
|
7
|
+
<% if flash['notice'] %>
|
8
|
+
<center>
|
9
|
+
<div class="flash">
|
10
|
+
<%=h flash['notice'] %>
|
11
|
+
</div>
|
12
|
+
</center>
|
13
|
+
<% end %>
|
14
|
+
<%= @content_for_layout %>
|
15
|
+
</body>
|
16
|
+
</html>
|
data/templates/user.rb
ADDED
@@ -0,0 +1,328 @@
|
|
1
|
+
require 'digest/sha2'
|
2
|
+
require 'model_security'
|
3
|
+
|
4
|
+
# A generic user login facility. Provides a user login, password
|
5
|
+
# management, and administrative facilities. Logs users in via HTTP Basic
|
6
|
+
# authentication, a login form, or a security token. Maintains the login
|
7
|
+
# state using Session.
|
8
|
+
#
|
9
|
+
# I started out with the Salted Hash login generator, and essentially rewrote
|
10
|
+
# the whole thing, learning a lot from the previous versions. This is not a
|
11
|
+
# criticism of the previous work, my goals were different. So, it's fair to
|
12
|
+
# say that this is derived from the work of Joe Hosteny and Tobias Leutke.
|
13
|
+
#
|
14
|
+
class User < ActiveRecord::Base
|
15
|
+
# This causes the security features to be added to the model.
|
16
|
+
include ModelSecurity
|
17
|
+
|
18
|
+
private
|
19
|
+
attr_accessible :login, :name, :email, :password, :password_confirmation, \
|
20
|
+
:old_password
|
21
|
+
|
22
|
+
# Hash a given password with the salt. This method localizes the encryption
|
23
|
+
# function so that it can be easily changed.
|
24
|
+
def encypher(s)
|
25
|
+
Digest::SHA512.hexdigest(salt + s)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a new user record.
|
29
|
+
#
|
30
|
+
# This is either used to create an ephemeral prototype object to initialize
|
31
|
+
# a form, or an object resulting from a form submission that will become a
|
32
|
+
# persistent record.
|
33
|
+
#
|
34
|
+
def initialize(attributes = nil)
|
35
|
+
super
|
36
|
+
|
37
|
+
if password
|
38
|
+
@password_is_new = true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns true if it is intended that the password be replaced when this
|
43
|
+
# record is saved.
|
44
|
+
def password_new?
|
45
|
+
@password_is_new
|
46
|
+
end
|
47
|
+
|
48
|
+
Char64 = (('a'..'z').collect + ('A'..'Z').collect + ('0'..'9').collect + ['.','/']).freeze
|
49
|
+
|
50
|
+
# Create a security token for use in logging in a user who has forgotten
|
51
|
+
# his password or has just created his login.
|
52
|
+
def token_string(n)
|
53
|
+
s = ""
|
54
|
+
n.times { s << Char64[(Time.now.tv_usec * rand) % 64] }
|
55
|
+
s
|
56
|
+
end
|
57
|
+
|
58
|
+
# Validates that initialize() sets @password_is_new to true, so that
|
59
|
+
# password validation works correctly. This would fail only in the case
|
60
|
+
# of a programming error.
|
61
|
+
def validate_on_create
|
62
|
+
password_new?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Validates that if we're changing the password or email, the old password
|
66
|
+
# has been given and matches the record. This is a defense against
|
67
|
+
# cookie-capture attacks.
|
68
|
+
def validate_on_update
|
69
|
+
if not (id.nil? or User.admin?) and (password_new? or @email_is_new)
|
70
|
+
if encypher(old_password) != cypher
|
71
|
+
errors.add(:old_password, "The old password doesn't match.")
|
72
|
+
return false
|
73
|
+
end
|
74
|
+
end
|
75
|
+
true
|
76
|
+
end
|
77
|
+
|
78
|
+
before_save :prepare_save
|
79
|
+
|
80
|
+
validates_presence_of :login
|
81
|
+
validates_uniqueness_of :login
|
82
|
+
validates_uniqueness_of :email
|
83
|
+
validates_format_of :email, \
|
84
|
+
:with => /^([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})$/
|
85
|
+
validates_presence_of :password, :if => :password_new?
|
86
|
+
validates_presence_of :password_confirmation, :if => :password_new?
|
87
|
+
validates_length_of :login, :within => 3..80
|
88
|
+
validates_length_of :password, :within => 5..128, :if => :password_new?
|
89
|
+
validates_confirmation_of :password, :if => :password_new?
|
90
|
+
|
91
|
+
# Here are the new model security specifications.
|
92
|
+
|
93
|
+
# These just control display.
|
94
|
+
let_display :all, :if => :never?
|
95
|
+
let_display :login, :name, :email
|
96
|
+
|
97
|
+
# These control both reading and writing.
|
98
|
+
|
99
|
+
# Let the administrator access all data. This implements a Unix-like
|
100
|
+
# super-user. Note that the coarse-grained override of the super-user
|
101
|
+
# is not a _necessary_ pattern for the ModelSecurity module, you can
|
102
|
+
# implement controls as fine-grained as you like.
|
103
|
+
let_access :all, :if => :admin?
|
104
|
+
|
105
|
+
# These control reading of model attributes.
|
106
|
+
|
107
|
+
# The controller before_filter require_admin tests if the current user
|
108
|
+
# is the administrator, thus we have to make the admin attribute readable
|
109
|
+
# by all. We would not have to make it readable were admin? only being
|
110
|
+
# used as a security test, as security tests can access all attributes
|
111
|
+
# with impunity. Login and name are public information.
|
112
|
+
#
|
113
|
+
# FIX: Reading "id" is necessary for Rails internals.
|
114
|
+
# Make it readable by default in the ModelSecurity module.
|
115
|
+
let_read :admin, :id, :login, :name
|
116
|
+
|
117
|
+
# If this is a new (never saved) record, or if this record corresponds to
|
118
|
+
# the currently-logged-in user, allow reading of the email address,
|
119
|
+
# timestamps and lock_version.
|
120
|
+
#
|
121
|
+
# FIX: Lock_version is read by rails internals when a record is saved,
|
122
|
+
# make it readable by default in the ModelSecurity module.
|
123
|
+
let_read :created_on, :email, :updated_on, :lock_version, :if => :new_or_me_or_logging_in?
|
124
|
+
|
125
|
+
# These attributes are concerned with login security, and can only be read
|
126
|
+
# while a user is logging in. We create a pseudo-user for the process of
|
127
|
+
# logging in and a security test :logging_in? that tests for that user.
|
128
|
+
let_read :activated, :cypher, :salt, :token, :token_expiry, \
|
129
|
+
:if => :logging_in?
|
130
|
+
|
131
|
+
# These control writing of model attributes.
|
132
|
+
|
133
|
+
# Only in the case of a new (never saved) record can these fields be written.
|
134
|
+
#
|
135
|
+
# FIX: Rails internals writes the ID and created_on. Make this test a
|
136
|
+
# default for :id and :created_on within the ModelSecurity module. Document
|
137
|
+
# it to the user.
|
138
|
+
let_write :id, :created_on, :login, :name, :if => :new_record?
|
139
|
+
|
140
|
+
# Only allow this information to be updated by the user who owns the record,
|
141
|
+
# unless this record is new (has never been saved).
|
142
|
+
#
|
143
|
+
# FIX: There should be a default write permission for updated_on and
|
144
|
+
# lock_version within the ModelSecurity module, as Rails internals
|
145
|
+
# write them.
|
146
|
+
let_write :cypher, :email, :salt, :if => :new_or_me?
|
147
|
+
|
148
|
+
# The security token can only be changed if we're the special "login" user.
|
149
|
+
let_write :activated, :token, :token_expiry, :if => :logging_in?
|
150
|
+
|
151
|
+
let_write :updated_on, :lock_version, :if => :new_or_me_or_logging_in?
|
152
|
+
|
153
|
+
public
|
154
|
+
attr_accessor :password, :password_confirmation, :old_password
|
155
|
+
|
156
|
+
# Change the user's password. Confirm the old password while doing so.
|
157
|
+
def change_password(attributes)
|
158
|
+
@password_is_new = true
|
159
|
+
self.password = attributes['password']
|
160
|
+
self.password_confirmation = attributes['password_confirmation']
|
161
|
+
self.old_password = attributes['old_password']
|
162
|
+
end
|
163
|
+
|
164
|
+
# Change the user's email address.
|
165
|
+
# FIX: send confirmation email.
|
166
|
+
def change_email(attributes)
|
167
|
+
@email_is_new = true
|
168
|
+
self.email = attributes['email']
|
169
|
+
self.old_password = attributes['old_password']
|
170
|
+
end
|
171
|
+
|
172
|
+
# Return the currently-logged-in user.
|
173
|
+
def User.current
|
174
|
+
@current_user
|
175
|
+
end
|
176
|
+
|
177
|
+
# Set the currently-logged-in user.
|
178
|
+
def User.current=(u)
|
179
|
+
@current_user = u
|
180
|
+
end
|
181
|
+
|
182
|
+
# Return true if this record corresponds to the currently-logged-in user.
|
183
|
+
# This is used as a security test.
|
184
|
+
def me?
|
185
|
+
User.current and User.current.id == id
|
186
|
+
end
|
187
|
+
|
188
|
+
# Return true if the currently-logged-in user is the administrator.
|
189
|
+
# Class method. This is used as a pseudo-security test by let_display.
|
190
|
+
def User.admin?
|
191
|
+
return ((current != nil ) and (current.admin.to_i == 1))
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return true if the currently-logged-in user is the administrator.
|
195
|
+
# Instance method. This is used as a security test.
|
196
|
+
def admin?
|
197
|
+
return User.admin?
|
198
|
+
end
|
199
|
+
|
200
|
+
# Return true if the user is currently logging in. This security test allows
|
201
|
+
# us to designate model fields to be visible *only* while a user is logging
|
202
|
+
# in.
|
203
|
+
def logging_in?
|
204
|
+
# FIX: create a real login user.
|
205
|
+
return User.current.nil?
|
206
|
+
end
|
207
|
+
|
208
|
+
def User.login_user
|
209
|
+
# FIX: create a real login user.
|
210
|
+
nil
|
211
|
+
end
|
212
|
+
|
213
|
+
# Return true if the user record is new (never been saved) or if it
|
214
|
+
# corresponds to the currently-logged-in user. This security test is
|
215
|
+
# a common pattern applied to a number of user record attributes.
|
216
|
+
def new_or_me?
|
217
|
+
new_record? or User.current == self
|
218
|
+
end
|
219
|
+
|
220
|
+
# Return true if the user record is new (never been saved) or if it
|
221
|
+
# corresponds to the currently-logged-in user, or if the current user
|
222
|
+
# is the special "login" user. This security test is a common pattern
|
223
|
+
# applied to a number of user record attributes.
|
224
|
+
def new_or_me_or_logging_in?
|
225
|
+
new_record? or User.current == self or logging_in?
|
226
|
+
end
|
227
|
+
|
228
|
+
# Create a new security token, or if the current one is not yet expired,
|
229
|
+
# return the current one. Should only be called with nobody logged in, it
|
230
|
+
# will log out the current user if one is logged in.
|
231
|
+
# Instance method.
|
232
|
+
def new_token
|
233
|
+
User.current = User.login_user
|
234
|
+
if token == '' or token_expiry < Time.now
|
235
|
+
self.token = token_string(10)
|
236
|
+
self.token_expiry = 7.days.from_now
|
237
|
+
result = save
|
238
|
+
end
|
239
|
+
User.current = nil
|
240
|
+
return token
|
241
|
+
end
|
242
|
+
|
243
|
+
# Create a new security token, or if the current one is not yet expired,
|
244
|
+
# return the current one. Should only be called with nobody logged in, it
|
245
|
+
# will log out the current user if one is logged in.
|
246
|
+
# Class method.
|
247
|
+
def User.new_token(address)
|
248
|
+
u = User.find_first(['email = ?', email])
|
249
|
+
u.new_token
|
250
|
+
end
|
251
|
+
|
252
|
+
# Encrypt the password before saving. Then wipe out the provided plaintext
|
253
|
+
# password, so that it won't trigger unnecessary security tests and
|
254
|
+
# validations the next time this record is saved. Wiping out the plaintext
|
255
|
+
# is more secure, anyway.
|
256
|
+
def prepare_save
|
257
|
+
# The salt is used to add a random factor to the plaintext. This might
|
258
|
+
# make some cryptographic attacks more difficult.
|
259
|
+
if password_new?
|
260
|
+
self.salt = token_string(40)
|
261
|
+
self.cypher = encypher(password)
|
262
|
+
|
263
|
+
self.password = nil
|
264
|
+
self.password_confirmation = nil
|
265
|
+
@password_is_new = nil
|
266
|
+
end
|
267
|
+
true
|
268
|
+
end
|
269
|
+
|
270
|
+
# Log off the current user.
|
271
|
+
def User.sign_off
|
272
|
+
User.current = nil
|
273
|
+
end
|
274
|
+
|
275
|
+
# Log on the user for this record, given a password. Instance method.
|
276
|
+
def sign_on(pass)
|
277
|
+
User.current = User.login_user
|
278
|
+
begin
|
279
|
+
if (activated == 1) and (pass != nil) and (encypher(pass) == cypher)
|
280
|
+
return (User.current = self)
|
281
|
+
end
|
282
|
+
rescue
|
283
|
+
end
|
284
|
+
User.current = nil
|
285
|
+
end
|
286
|
+
|
287
|
+
# Log on the user for this record, given a user name and password.
|
288
|
+
# Class method.
|
289
|
+
def User.sign_on(handle, pwd)
|
290
|
+
user = find_first(['login = ?', handle])
|
291
|
+
if user
|
292
|
+
user.sign_on(pwd)
|
293
|
+
else
|
294
|
+
nil
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
298
|
+
# Continue the current login, from the session data.
|
299
|
+
# This should be called by User.setup .
|
300
|
+
def User.sign_on_by_session(user_id)
|
301
|
+
begin
|
302
|
+
return (User.current = User.find(user_id))
|
303
|
+
rescue
|
304
|
+
end
|
305
|
+
return nil
|
306
|
+
end
|
307
|
+
|
308
|
+
# Sign on the user using a security token. Instance method.
|
309
|
+
def sign_on_by_token(t)
|
310
|
+
User.current = User.login_user
|
311
|
+
if t == token and (token_expiry >= Time.now)
|
312
|
+
self.token = ""
|
313
|
+
self.token_expiry = Time.now
|
314
|
+
self.activated = 1
|
315
|
+
save
|
316
|
+
User::current = self
|
317
|
+
return self;
|
318
|
+
end
|
319
|
+
return nil
|
320
|
+
end
|
321
|
+
|
322
|
+
# Sign on the user using an ID (record index) and security token.
|
323
|
+
# Class method.
|
324
|
+
def User.sign_on_by_token(id, token)
|
325
|
+
u = User.find(id)
|
326
|
+
return u.sign_on_by_token(token)
|
327
|
+
end
|
328
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# The HTTP authorization code is
|
2
|
+
# derived from an example published by Maximillian Dornseif at
|
3
|
+
# http://blogs.23.nu/c0re/stories/7409/
|
4
|
+
# which was released for use under any license.
|
5
|
+
#
|
6
|
+
# I started out with the Salted Hash login generator, and essentially rewrote
|
7
|
+
# the whole thing, learning a lot from the previous versions. This is not a
|
8
|
+
# criticism of the previous work, my goals were different. So, it's fair to
|
9
|
+
# say that this is derived from the work of Joe Hosteny and Tobias Leutke.
|
10
|
+
#
|
11
|
+
class UserController < ApplicationController
|
12
|
+
# helper ModelSecurity
|
13
|
+
|
14
|
+
private
|
15
|
+
# Require_admin will require an administrative login before the action
|
16
|
+
# can be called. It uses Modal, so it will continue to the action if the
|
17
|
+
# login is successful.
|
18
|
+
before_filter :require_admin, :only => [ :destroy ]
|
19
|
+
|
20
|
+
# Require_login will require a login before the action
|
21
|
+
# can be called. It uses Modal, so it will continue to the action if the
|
22
|
+
# login is successful.
|
23
|
+
before_filter :require_login, :only => [ :list, :show ]
|
24
|
+
|
25
|
+
# Cause HTTP Basic validation.
|
26
|
+
# FIX: Basic is not secure. Change to Digest authentication.
|
27
|
+
def http_authorize(realm=@request.domain(2))
|
28
|
+
# This will cause the browser to send HTTP authorization information.
|
29
|
+
@response.headers["Status"] = "Unauthorized"
|
30
|
+
@response.headers["WWW-Authenticate"] = "Basic realm=\"#{realm}\""
|
31
|
+
render :status => 401
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize
|
35
|
+
end
|
36
|
+
|
37
|
+
public
|
38
|
+
scaffold :user
|
39
|
+
|
40
|
+
private
|
41
|
+
public
|
42
|
+
|
43
|
+
# Activate a new user, having logged in with a security token. All of the
|
44
|
+
# work goes on in user_support.
|
45
|
+
def activate
|
46
|
+
end
|
47
|
+
|
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
|
+
# Send out a forgot-password email.
|
71
|
+
def forgot_password
|
72
|
+
case @request.method
|
73
|
+
when :get
|
74
|
+
@user = User.new
|
75
|
+
when :post
|
76
|
+
@user = User.find_first(['email = ?', @params['user']['email']])
|
77
|
+
if @user
|
78
|
+
url = url_for(:controller => 'user', :action => 'activate', :id => @user.id, :token => @user.new_token)
|
79
|
+
UserMailer.deliver_forgot_password(@user, url)
|
80
|
+
render :action => 'forgot_password_done'
|
81
|
+
else
|
82
|
+
flash['notice'] = "Can't find a user with email #{@params['email']}."
|
83
|
+
@user = User.new
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
# Tell the user the email's on the way.
|
89
|
+
def forgot_password_done
|
90
|
+
end
|
91
|
+
|
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
|
+
# Attempt HTTP authentication, and fall back on a login form.
|
106
|
+
# If this method is called login_admin (it's an alias), keep trying
|
107
|
+
# until an administrator logs in or the user pushes the "back" button.
|
108
|
+
def login
|
109
|
+
if User.current and (admin? or action_name != 'login_admin')
|
110
|
+
redirect_back_or_default :action => :success
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
@user = User.new
|
115
|
+
|
116
|
+
http_authorize
|
117
|
+
end
|
118
|
+
|
119
|
+
# Log in an administrator. If a non-administrator logs in, keep trying
|
120
|
+
# until an administrator logs in or the user pushes the "back" button.
|
121
|
+
alias login_admin login
|
122
|
+
|
123
|
+
# Log out the current user, attempt HTTP authentication to log in a new
|
124
|
+
# user. The real reason we put up HTTP authentication here is to make
|
125
|
+
# the browser forget the old authentication data. Otherwise, the browser
|
126
|
+
# keeps sending it!
|
127
|
+
def logout
|
128
|
+
if User.current and @session[:new_login] != true
|
129
|
+
@session[:new_login] = true
|
130
|
+
User.sign_off
|
131
|
+
session[:user_id] = nil
|
132
|
+
@response.headers["Status"] = "Unauthorized"
|
133
|
+
@response.headers["WWW-Authenticate"] = \
|
134
|
+
"Basic realm=\"#{@request.domain(2)}\""
|
135
|
+
@user = User.new
|
136
|
+
render :action => 'login', :status => 401
|
137
|
+
else
|
138
|
+
@session[:new_login] = false
|
139
|
+
if User.current
|
140
|
+
redirect_to :action => 'success'
|
141
|
+
else
|
142
|
+
redirect_to :action => 'login'
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Create a new user.
|
148
|
+
def new
|
149
|
+
case @request.method
|
150
|
+
when :get
|
151
|
+
@user = User.new
|
152
|
+
when :post
|
153
|
+
@user = User.new(@params['user'])
|
154
|
+
# FIX: Save may fail. Create the email before the record is saved,
|
155
|
+
# deliver it afterward.
|
156
|
+
url = url_for(:controller => 'user', :action => 'activate', :id => @user.id, :token => @user.new_token)
|
157
|
+
UserMailer.deliver_new_user(@user, url)
|
158
|
+
if @user.save
|
159
|
+
flash['notice'] = 'User created.'
|
160
|
+
redirect_to :action => 'success'
|
161
|
+
else
|
162
|
+
flash['notice'] = 'Creation of new user failed.'
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Display a user's information.
|
168
|
+
# FIX: Use the user's login instead of the record ID.
|
169
|
+
# Using the record ID provides an easy way for outsiders to enumerate the
|
170
|
+
# users, which has security implications.
|
171
|
+
def show
|
172
|
+
@user = User.find(@params[:id])
|
173
|
+
end
|
174
|
+
|
175
|
+
# Tell the user that an action succeeded.
|
176
|
+
def success
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
2
|
+
require 'user_controller'
|
3
|
+
|
4
|
+
# Raise errors beyond the default web-based presentation
|
5
|
+
class UserController; def rescue_action(e) raise e end; end
|
6
|
+
|
7
|
+
class UserControllerTest < Test::Unit::TestCase
|
8
|
+
fixtures :users
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@controller = UserController.new
|
12
|
+
@request, @response = ActionController::TestRequest.new, ActionController::TestResponse.new
|
13
|
+
@request.host = "localhost"
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_true
|
17
|
+
true
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|