Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 167
- Log:
Implemented account enumeration. Improved pagination and enumeration
"show details" mechanism; the HTTP referrer is used to return to the
list view to get the right page and list type. Obvious potential for
trouble if someone directly links to an item from an external site
but since the details items require administrative Hub access it is
unlikely to present a problem in real use.
- Author:
- rool
- Date:
- Mon Jan 01 14:14:35 +0000 2007
- Size:
- 18112 Bytes
1 | class AccountController < ApplicationController |
2 | |
3 | layout 'default.rhtml' |
4 | |
5 | # Action permissions for this class as a class variable, exposed |
6 | # to the public through a class method. |
7 | |
8 | @@hubssolib_permissions = HubSsoLib::Permissions.new({ |
9 | :change_password => [ :admin, :webmaster, :privileged, :normal ], |
10 | :change_details => [ :admin, :webmaster, :privileged, :normal ], |
11 | :delete => [ :admin, :webmaster, :privileged, :normal ], |
12 | :delete_confirm => [ :admin, :webmaster, :privileged, :normal ], |
13 | :list => [ :admin, :webmaster, :privileged ], |
14 | :enumerate => [ :admin, :webmaster ], |
15 | :show => [ :admin, :webmaster ], |
16 | :edit_roles => [ :admin ], |
17 | :destroy => [ :admin ] |
18 | }) |
19 | |
20 | def AccountController.hubssolib_permissions |
21 | @@hubssolib_permissions |
22 | end |
23 | |
24 | # Set up the notification mailer. |
25 | |
26 | observer :user_observer |
27 | |
28 | # HTTPS enforcement for all methods, except the login indicator; if someone |
29 | # is on an HTTP page, the login indicator needs to be fetched by HTTP too so |
30 | # it can show "logged out" as the secure-only cookies won't get sent. It is |
31 | # very confusing to be on an HTTP page, apparently fetching the indicator by |
32 | # HTTP, only to have the image fetch quietly redirect behind the scenes, go |
33 | # to HTTPS, and say you're logged in - when everyone else thinks you're not. |
34 | |
35 | before_filter :hubssolib_ensure_https, :except => :login_indication |
36 | |
37 | # The "proper" login method |
38 | # |
39 | def login |
40 | @title = 'Log in' |
41 | return unless request.post? |
42 | |
43 | @email = params[:email] |
44 | self.hubssolib_current_user = from_real_user(User.authenticate(@email, params[:password])) |
45 | |
46 | if (self.hubssolib_current_user and self.hubssolib_current_user != :false) |
47 | hubssolib_set_last_used(Time.now.utc) |
48 | |
49 | privileges = hubssolib_get_user_roles.to_human_s.downcase |
50 | hubssolib_set_flash(:notice, "Logged in successfully. Welcome, #{hubssolib_get_user_name}. You have #{privileges} privileges.") |
51 | hubssolib_redirect_back_or_default(:controller => 'tasks', :action => nil) |
52 | else |
53 | flash[:alert] = 'Incorrect e-mail address or password.' |
54 | end |
55 | end |
56 | |
57 | # Log out the user and redirect to the Tasks controller. |
58 | # |
59 | def logout |
60 | @title = 'Log out' |
61 | hubssolib_log_out() |
62 | flash[:attention] = 'You are now logged out.' |
63 | redirect_to :controller => 'tasks', :action => nil |
64 | end |
65 | |
66 | def signup |
67 | @title = 'Sign up' |
68 | return unless request.post? |
69 | |
70 | # Bulk assignment from the params hash is safe because the User object |
71 | # contains nothing that won't be overwritten anyway or isn't already |
72 | # protected by attr_accessible in the User model. |
73 | |
74 | @user = User.new(params[:user]) |
75 | |
76 | # Are there any users yet? If not, grant this user admin permissions. |
77 | # Administrators are for just this application; whether or not admin |
78 | # privileges affect other applications depends on the level of external |
79 | # SSO integration. |
80 | |
81 | users = User.find_all |
82 | |
83 | if (users.empty? or users.nil?) |
84 | @user.roles = HubSsoLib::Roles.new(true).to_s |
85 | @user.save! |
86 | @user.activate |
87 | self.hubssolib_current_user = from_real_user(@user) |
88 | |
89 | flash[:notice] = 'Thanks for signing up. You are now the system administrator ' << |
90 | 'and your account has been automatically activated.' |
91 | else |
92 | |
93 | @user.roles = HubSsoLib::Roles.new(false).to_s |
94 | @user.save! |
95 | |
96 | flash[:notice] = 'Thanks for signing up. Your site account must be activated ' << |
97 | 'before you can use it - please check your e-mail account ' << |
98 | 'for a message which tells you what you should do next.' |
99 | end |
100 | |
101 | redirect_to :controller => 'tasks', :action => nil |
102 | |
103 | rescue ActiveRecord::RecordInvalid |
104 | render :action => 'signup' |
105 | end |
106 | |
107 | def activate |
108 | activation_code = params[:activation_code] || params[:id] |
109 | |
110 | unless activation_code.nil? |
111 | @user = User.find_by_activation_code(activation_code) |
112 | |
113 | if @user and @user.activate |
114 | self.hubssolib_current_user = from_real_user(@user) |
115 | |
116 | hubssolib_set_flash(:notice, 'Your RISC OS Open web site account is now active.') |
117 | hubssolib_redirect_back_or_default(:controller => 'tasks', :action => nil) |
118 | else |
119 | flash[:alert] = 'Unable to activate your RISC OS Open web site account. ' << |
120 | 'Is the activation code correct, or has it already been used? ' << |
121 | 'If in doubt please try to sign up again. Contact ROOL if you ' << |
122 | 'keep having trouble.' |
123 | |
124 | redirect_to :controller => 'account', :action => 'signup' |
125 | end |
126 | else |
127 | redirect_to :controller => 'account', :action => 'signup' |
128 | end |
129 | end |
130 | |
131 | def change_password |
132 | @title = 'Change password' |
133 | return unless request.post? |
134 | |
135 | user = to_real_user(self.hubssolib_current_user) |
136 | |
137 | if User.authenticate(user.email, params[:old_password]) |
138 | if (params[:password] == params[:password_confirmation]) |
139 | user.password_confirmation = params[:password_confirmation] |
140 | user.password = params[:password] |
141 | save_password_and_set_flash(user) |
142 | self.hubssolib_current_user = from_real_user(user) |
143 | |
144 | redirect_to :controller => 'tasks', :action => nil |
145 | else |
146 | set_password_mismatch_flash |
147 | @old_password = params[:old_password] |
148 | end |
149 | else |
150 | flash[:alert] = 'Incorrect current password.' |
151 | end |
152 | end |
153 | |
154 | def change_details |
155 | @title = 'Update account details' |
156 | @user = to_real_user(self.hubssolib_current_user) |
157 | @real_name = @user ? @user.real_name || '' : '' |
158 | |
159 | return unless request.post? |
160 | |
161 | if (params[:real_name]) |
162 | @user.real_name = @real_name = params[:real_name] |
163 | @user.save! |
164 | self.hubssolib_current_user = from_real_user(@user) |
165 | |
166 | flash[:notice] = 'Account details updated successfully.' |
167 | redirect_to :controller => 'tasks', :action => nil |
168 | end |
169 | end |
170 | |
171 | def forgot_password |
172 | @title = 'Forgotten password' |
173 | return unless request.post? |
174 | |
175 | if @user = User.find_by_email(params[:email]) |
176 | @user.forgot_password |
177 | @user.save! |
178 | |
179 | flash[:notice] = 'An e-mail message which tells you how to reset your ' << |
180 | 'account password has been set to your e-mail address.' |
181 | |
182 | redirect_to :controller => 'tasks', :action => nil |
183 | else |
184 | flash[:alert] = 'No account was found for the given e-mail address.' |
185 | end |
186 | end |
187 | |
188 | def reset_password |
189 | @title = 'Reset password' |
190 | |
191 | if params[:id].nil? |
192 | hubssolib_redirect_back_or_default(:controller => 'tasks', :action => nil) |
193 | return |
194 | end |
195 | |
196 | @user = User.find_by_password_reset_code(params[:id]) |
197 | |
198 | if (@user.nil?) |
199 | hubssolib_set_flash(:alert, 'Invalid reset code. Did your e-mail client break up the reset ' << |
200 | 'link so it spanned more than one line? If so, please try again, ' << |
201 | 'copying all of the link in the message however many lines it spans.') |
202 | |
203 | hubssolib_redirect_back_or_default(:controller => 'tasks', :action => nil) |
204 | return |
205 | end |
206 | |
207 | t = Time.now.utc |
208 | if (t >= (@user.password_reset_code_expires_at || t)) # Allows for 'nil' in expiry field |
209 | flash[:alert] = 'The reset code has expired. Please try your reset request again.' |
210 | redirect_to :controller => 'account', :action => 'forgot_password' |
211 | return |
212 | end |
213 | |
214 | unless params[:password] |
215 | flash[:alert] = 'Reset your password using the form below.' |
216 | return |
217 | end |
218 | |
219 | if (params[:password] == params[:password_confirmation]) |
220 | @user.password_confirmation = params[:password_confirmation] |
221 | @user.password = params[:password] |
222 | @user.reset_password |
223 | save_password_and_set_flash(@user) |
224 | self.hubssolib_current_user = from_real_user(@user) |
225 | redirect_to :controller => 'tasks', :action => nil |
226 | return |
227 | else |
228 | set_password_mismatch_flash |
229 | return |
230 | end |
231 | end |
232 | |
233 | def delete |
234 | flash[:alert] = 'Are you sure?' |
235 | title = 'Delete account: Are you sure?' |
236 | end |
237 | |
238 | def delete_confirm |
239 | me = to_real_user(self.hubssolib_current_user) |
240 | hubssolib_log_out() |
241 | me.destroy |
242 | |
243 | flash.clear |
244 | flash[:attention] = 'Your account has been deleted.' |
245 | redirect_to :controller => 'tasks', :action => nil |
246 | end |
247 | |
248 | def list |
249 | @title = 'List of user accounts' |
250 | |
251 | # Page number zero is magic; it indicates "all items". |
252 | |
253 | count = User.count |
254 | |
255 | if (params[:page] == '0') |
256 | limit = count |
257 | @all = true |
258 | else |
259 | limit = 10 |
260 | @all = false |
261 | end |
262 | |
263 | @pager = ::Paginator.new(count, limit) do |offset, per_page| |
264 | User.find(:all, :limit => per_page, :offset => offset) |
265 | end |
266 | |
267 | @page = @pager.page(params[:page]) |
268 | end |
269 | |
270 | # Enumerate active users (those users known to the DRb server). |
271 | # |
272 | def enumerate |
273 | @title = 'Active users' |
274 | @users = [] |
275 | users = hubssolib_enumerate_users |
276 | |
277 | # Map the user objects returned from the HubSsoLib Gem to |
278 | # internal users. |
279 | |
280 | users.each do |user| |
281 | @users.push(to_real_user(user)) if (user && user.user_id) |
282 | end |
283 | |
284 | count = @users.length |
285 | |
286 | if (count > 0) |
287 | |
288 | # Page number zero is magic; it indicates "all items". |
289 | |
290 | if (params[:page] == '0') |
291 | limit = count |
292 | @all = true |
293 | else |
294 | limit = 10 |
295 | @all = false |
296 | end |
297 | |
298 | @pager = ::Paginator.new(count, limit) do |offset, per_page| |
299 | @users[offset..(offset + per_page - 1)] |
300 | end |
301 | |
302 | @page = @pager.page(params[:page]) |
303 | |
304 | else |
305 | |
306 | @pager = false |
307 | |
308 | end |
309 | end |
310 | |
311 | # Show details of a specific user account. |
312 | # |
313 | def show |
314 | @title = 'User account details' |
315 | @user = User.find(params[:id]) |
316 | @referrer = request.env["HTTP_REFERER"] |
317 | @referrer = nil unless (@referrer && !@referrer.empty?) |
318 | end |
319 | |
320 | def edit_roles |
321 | @title = 'Edit account roles' |
322 | |
323 | # We must have a valid ID |
324 | |
325 | unless (request.post?) and (params[:id]) and (@user = User.find(params[:id])) |
326 | redirect_to :controller => tasks, :action => nil |
327 | return |
328 | end |
329 | |
330 | # If 'commit' is present, the form was submitted with details rather than |
331 | # visited from a list or account details view. |
332 | |
333 | return unless (params[:commit]) |
334 | |
335 | # Validate the result |
336 | |
337 | roles = (params[:user] ? params[:user][:roles] : '').to_authenticated_roles |
338 | |
339 | unless (roles.validate) |
340 | flash[:alert] = 'Invalid roles chosen. Ensure at least one item in the list is selected.' |
341 | else |
342 | @user.roles = roles.to_s |
343 | @user.save! |
344 | |
345 | # Did I update my own roles? |
346 | |
347 | if (hubssolib_get_user_id == @user.id) |
348 | self.hubssolib_current_user = from_real_user(@user) |
349 | end |
350 | |
351 | flash[:notice] = 'Account roles updated successfully.' |
352 | redirect_to :action => 'show', :id => @user.id |
353 | end |
354 | end |
355 | |
356 | def destroy |
357 | user = User.find(params[:id]) |
358 | |
359 | if (hubssolib_get_user_id == user.id) |
360 | flash[:alert] = 'Please use the normal control panel to delete your own account.' |
361 | elsif (user.roles.to_authenticated_roles.include?(:admin)) |
362 | flash[:alert] = 'You cannot destroy an administrator account from here! ' << |
363 | 'You can only do that at the control panel when ' << |
364 | 'logged into the account, or at the database level.' |
365 | else |
366 | user.destroy |
367 | flash[:alert] = 'The account has been deleted.' |
368 | end |
369 | |
370 | redirect_to :action => 'list' |
371 | end |
372 | |
373 | # The login_indication method is unusual; it returns data for an image, |
374 | # with no-cache parameters set, to indicate whether or not the user is |
375 | # logged in. It does not render a view. |
376 | # |
377 | # The idea is that a caller which caches HTML can include an image tag |
378 | # that points its source data to this method; the image will be updated |
379 | # even if the HTML stays cached. |
380 | # |
381 | def login_indication |
382 | file = "#{LOGIN_ICONS}#{hubssolib_logged_in? ? '/logged_in.png' : '/not_logged_in.png'}" |
383 | |
384 | @headers['Pragma'] = 'no-cache' |
385 | @headers['Cache-Control'] = 'no-cache, must-revalidate' |
386 | |
387 | send_file file, |
388 | :type => 'image/png', |
389 | :disposition => 'inline' |
390 | end |
391 | |
392 | # A supporting unusual method is login_conditional, which redirects to |
393 | # the login page if the user is logged out or the tasks page if the user |
394 | # is logged in. It explicitly clears a return-to link, if there is one, |
395 | # so that the user doesn't drop out of Hub. This is useful if the page |
396 | # from which the user came cannot support (for example) the Flash display |
397 | # because of, say, caching. |
398 | # |
399 | def login_conditional |
400 | if (hubssolib_ensure_https) # Redirect back to here using HTTPS, if not already |
401 | hubssolib_store_location(nil) |
402 | |
403 | if (hubssolib_logged_in?) |
404 | redirect_to :controller => 'tasks', :action => nil |
405 | else |
406 | redirect_to :action => 'login' |
407 | end |
408 | end |
409 | end |
410 | |
411 | # The login_hop method is also unusual; it redirects to the login page |
412 | # if the user is logged out, or the tasks page if the user is logged in. |
413 | # For logging in, an attempt is made to remember the previous URL by HTTP |
414 | # 'referer' header, with the redirection post-login/logout set to go back |
415 | # to that page. |
416 | # |
417 | # The idea is that a caller which caches HTML can provide a link to this |
418 | # method which redirects to a more relevant location depending on the |
419 | # user login status. |
420 | # |
421 | def login_hop |
422 | ret = request.env["HTTP_REFERER"] |
423 | |
424 | if hubssolib_logged_in? |
425 | if (ret && !ret.empty?) |
426 | uri = URI.parse(ret) |
427 | |
428 | # There's a Referer header; did it come from an SSL connection? If |
429 | # so redirect to the control panel. If not, perhaps the only reason |
430 | # the user got bounced to the login_hop method was because they were |
431 | # logged in but had dropped back to HTTP, thus hiding their login |
432 | # state. Redirect back to the referer, but with HTTPS. Should they |
433 | # in turn bounce here *again*, we'll be over HTTPS this time, so will |
434 | # go onto the control panel. |
435 | |
436 | if (uri.scheme == 'https') |
437 | redirect_to :controller => 'tasks', :action => nil |
438 | else |
439 | hubssolib_set_flash(:attention, "You were logged in, but using an insecure connection to the web site. You've been moved onto a secure connection and may not need to log in again as a result.") |
440 | redirect_to hubssolib_promote_uri_to_ssl(ret) |
441 | end |
442 | else |
443 | |
444 | # There's no Referer header. We're logged in; show the control |
445 | # panel. |
446 | |
447 | redirect_to :controller => 'tasks', :action => nil |
448 | end |
449 | else |
450 | |
451 | # Try to store the referer header for future reference (won't work |
452 | # unless we're over HTTPS, but the HubSsoLib deals with some |
453 | # redirections related to that) and go to the login page. |
454 | |
455 | hubssolib_store_location(ret) if (ret && !ret.empty?) |
456 | redirect_to :action => 'login' |
457 | end |
458 | end |
459 | |
460 | protected |
461 | |
462 | # Pass a HubSsoLib::User object. Returns an equivalent User Model object. |
463 | # |
464 | def to_real_user(user) |
465 | return nil if user.nil? |
466 | raise 'Incorrect argument class' unless (user.class == HubSsoLib::User or user.class == DRbObject) |
467 | |
468 | # Unpleasant "user_" prefix in HubSsoLib::User field names is to avoid |
469 | # collisions (e.g. of "id") with DRbObject. |
470 | |
471 | real_user = User.find(user.user_id) |
472 | raise 'No equivalent real user' unless real_user |
473 | |
474 | real_user.salt = user.user_salt |
475 | real_user.roles = user.user_roles |
476 | real_user.activated_at = user.user_activated_at |
477 | real_user.real_name = user.user_real_name |
478 | real_user.crypted_password = user.user_crypted_password |
479 | real_user.remember_token_expires_at = user.user_remember_token_expires_at |
480 | real_user.activation_code = user.user_activation_code |
481 | real_user.member_id = user.user_member_id |
482 | real_user.password_reset_code = user.user_password_reset_code |
483 | real_user.remember_token = user.user_remember_token |
484 | real_user.email = user.user_email |
485 | real_user.password_reset_code_expires_at = user.user_password_reset_code_expires_at |
486 | |
487 | return real_user |
488 | end |
489 | |
490 | # Pass a User Model object. Returns an equivalent HubSsoLib::User object. |
491 | # |
492 | def from_real_user(real_user) |
493 | return nil if real_user.nil? |
494 | raise 'Incorrect argument class' unless real_user.class == User |
495 | |
496 | user = HubSsoLib::User.new |
497 | |
498 | user.user_salt = real_user.salt |
499 | user.user_roles = real_user.roles |
500 | user.user_updated_at = real_user.updated_at |
501 | user.user_activated_at = real_user.activated_at |
502 | user.user_real_name = real_user.real_name |
503 | user.user_crypted_password = real_user.crypted_password |
504 | user.user_remember_token_expires_at = real_user.remember_token_expires_at |
505 | user.user_activation_code = real_user.activation_code |
506 | user.user_member_id = real_user.member_id |
507 | user.user_id = real_user.id |
508 | user.user_password_reset_code = real_user.password_reset_code |
509 | user.user_remember_token = real_user.remember_token |
510 | user.user_email = real_user.email |
511 | user.user_created_at = real_user.created_at |
512 | user.user_password_reset_code_expires_at = real_user.password_reset_code_expires_at |
513 | |
514 | return user |
515 | end |
516 | |
517 | def save_password_and_set_flash(user) |
518 | user.save ? |
519 | flash[:notice] = 'Your password has been changed.' : |
520 | flash[:alert] = 'Sorry, your password could not be changed.' |
521 | end |
522 | |
523 | def set_password_mismatch_flash |
524 | flash[:alert] = 'The new password differed from the password confirmation you entered.' |
525 | end |
526 | end |