model_security_generator 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +0 -0
- data/USAGE +0 -0
- data/model_security_generator.rb +75 -0
- data/templates/_view_form.rhtml +27 -0
- data/templates/mailer_forgot_password.rhtml +18 -0
- data/templates/mailer_new_user.rhtml +10 -0
- data/templates/mock_mailer.rb +16 -0
- data/templates/mock_time.rb +17 -0
- data/templates/modal.rb +82 -0
- data/templates/modal_helper.rb +29 -0
- data/templates/model_security.rb +334 -0
- data/templates/model_security_helper.rb +64 -0
- data/templates/once.rb +36 -0
- data/templates/scaffold.css +74 -0
- data/templates/scaffold.rhtml +11 -0
- data/templates/schema.sql +4 -0
- data/templates/standard.css +7 -0
- data/templates/standard.rhtml +16 -0
- data/templates/user.rb +328 -0
- data/templates/user_controller.rb +178 -0
- data/templates/user_controller_test.rb +20 -0
- data/templates/user_mailer.rb +29 -0
- data/templates/user_support.rb +124 -0
- data/templates/user_test.rb +10 -0
- data/templates/users.sql +17 -0
- data/templates/users.yml +41 -0
- data/templates/view_activate.rhtml +1 -0
- data/templates/view_edit.rhtml +10 -0
- data/templates/view_forgot_password_done.rhtml +5 -0
- data/templates/view_list.rhtml +35 -0
- data/templates/view_login.rhtml +11 -0
- data/templates/view_login_admin.rhtml +16 -0
- data/templates/view_logout.rhtml +1 -0
- data/templates/view_new.rhtml +30 -0
- data/templates/view_show.rhtml +10 -0
- data/templates/view_success.rhtml +1 -0
- metadata +83 -0
data/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
|