ioquatix-account_engine 0.1.0

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.
Files changed (74) hide show
  1. data/README +0 -0
  2. data/lib/account_engine/configuration.rb +101 -0
  3. data/lib/account_engine/controller.rb +246 -0
  4. data/lib/account_engine/helper.rb +104 -0
  5. data/lib/account_engine/password.rb +432 -0
  6. data/lib/account_engine/support.rb +12 -0
  7. data/lib/account_engine/user_account/class_methods.rb +63 -0
  8. data/lib/account_engine/user_account.rb +184 -0
  9. data/lib/account_engine.rb +63 -0
  10. data/rails/app/controllers/account_controller.rb +162 -0
  11. data/rails/app/controllers/permissions_controller.rb +90 -0
  12. data/rails/app/controllers/roles_controller.rb +133 -0
  13. data/rails/app/controllers/users_controller.rb +144 -0
  14. data/rails/app/helpers/account_helper.rb +3 -0
  15. data/rails/app/helpers/permissions_helper.rb +3 -0
  16. data/rails/app/helpers/roles_helper.rb +3 -0
  17. data/rails/app/helpers/users_helper.rb +3 -0
  18. data/rails/app/models/permission.rb +129 -0
  19. data/rails/app/models/role.rb +60 -0
  20. data/rails/app/models/user.rb +5 -0
  21. data/rails/app/models/user_notify.rb +75 -0
  22. data/rails/app/views/account/_form.rhtml +8 -0
  23. data/rails/app/views/account/change_password.rhtml +17 -0
  24. data/rails/app/views/account/edit.rhtml +5 -0
  25. data/rails/app/views/account/forgot_password.rhtml +12 -0
  26. data/rails/app/views/account/home.rhtml +3 -0
  27. data/rails/app/views/account/login.rhtml +27 -0
  28. data/rails/app/views/account/logout.rhtml +8 -0
  29. data/rails/app/views/account/signup.rhtml +28 -0
  30. data/rails/app/views/permissions/_form.rhtml +14 -0
  31. data/rails/app/views/permissions/_list.rhtml +38 -0
  32. data/rails/app/views/permissions/edit.rhtml +5 -0
  33. data/rails/app/views/permissions/index.rhtml +3 -0
  34. data/rails/app/views/permissions/new.rhtml +5 -0
  35. data/rails/app/views/roles/_form.rhtml +8 -0
  36. data/rails/app/views/roles/_permissions.rhtml +25 -0
  37. data/rails/app/views/roles/edit.rhtml +5 -0
  38. data/rails/app/views/roles/index.rhtml +34 -0
  39. data/rails/app/views/roles/new.rhtml +5 -0
  40. data/rails/app/views/roles/show.rhtml +20 -0
  41. data/rails/app/views/user_notify/change_password.rhtml +10 -0
  42. data/rails/app/views/user_notify/delete.rhtml +5 -0
  43. data/rails/app/views/user_notify/forgot_password.rhtml +11 -0
  44. data/rails/app/views/user_notify/pending_delete.rhtml +9 -0
  45. data/rails/app/views/user_notify/signup.rhtml +12 -0
  46. data/rails/app/views/users/_form.rhtml +12 -0
  47. data/rails/app/views/users/edit.rhtml +5 -0
  48. data/rails/app/views/users/index.rhtml +38 -0
  49. data/rails/app/views/users/new.rhtml +5 -0
  50. data/rails/app/views/users/roles.rhtml +42 -0
  51. data/rails/app/views/users/show.rhtml +36 -0
  52. data/rails/assets/images/default/omnipotent.png +0 -0
  53. data/rails/assets/images/default/system.png +0 -0
  54. data/rails/assets/images/permissions/create.png +0 -0
  55. data/rails/assets/images/permissions/sync.png +0 -0
  56. data/rails/assets/images/roles/add_permission.png +0 -0
  57. data/rails/assets/images/roles/create.png +0 -0
  58. data/rails/assets/images/roles/edit.png +0 -0
  59. data/rails/assets/images/roles/remove_permission.png +0 -0
  60. data/rails/assets/images/roles/user.png +0 -0
  61. data/rails/assets/images/table_background.png +0 -0
  62. data/rails/assets/images/users/create.png +0 -0
  63. data/rails/assets/images/users/destroy.png +0 -0
  64. data/rails/assets/images/users/edit.png +0 -0
  65. data/rails/assets/images/users/show.png +0 -0
  66. data/rails/assets/javascripts/account_engine.js +166 -0
  67. data/rails/assets/stylesheets/account_engine.css +7 -0
  68. data/rails/assets/stylesheets/check_password.css +10 -0
  69. data/rails/assets/stylesheets/simple.css +168 -0
  70. data/rails/db/migrate/001_initial_schema.rb +49 -0
  71. data/rails/init.rb +21 -0
  72. data/rails/routes.rb +5 -0
  73. data/rails/tasks/account_engine.rake +123 -0
  74. metadata +165 -0
@@ -0,0 +1,432 @@
1
+ # $Id: password.rb,v 1.24 2006/03/02 19:42:33 ianmacd Exp $
2
+ #
3
+ # Version : 0.5.3
4
+ # Author : Ian Macdonald <ian@caliban.org>
5
+ #
6
+ # Copyright (C) 2002-2006 Ian Macdonald
7
+ #
8
+ # This program is free software; you can redistribute it and/or modify
9
+ # it under the terms of the GNU General Public License as published by
10
+ # the Free Software Foundation; either version 2, or (at your option)
11
+ # any later version.
12
+ #
13
+ # This program is distributed in the hope that it will be useful,
14
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
15
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16
+ # GNU General Public License for more details.
17
+ #
18
+ # You should have received a copy of the GNU General Public License
19
+ # along with this program; if not, write to the Free Software Foundation,
20
+ # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
21
+
22
+
23
+ #require 'crack'
24
+ #require 'termios'
25
+
26
+
27
+ # Ruby/Password is a collection of password handling routines for Ruby,
28
+ # including an interface to CrackLib for the purposes of testing password
29
+ # strength.
30
+ #
31
+ # require 'password'
32
+ #
33
+ # # Define and check a password in code
34
+ # pw = Password.new( "bigblackcat" )
35
+ # pw.check
36
+ #
37
+ # # Get and check a password from the keyboard
38
+ # begin
39
+ # password = Password.get( "New password: " )
40
+ # password.check
41
+ # rescue Password::WeakPassword => reason
42
+ # puts reason
43
+ # retry
44
+ # end
45
+ #
46
+ # # Automatically generate and encrypt a password
47
+ # password = Password.phonemic( 12, Password:ONE_CASE | Password::ONE_DIGIT )
48
+ # crypted = password.crypt
49
+ #
50
+ #
51
+ class Password < String
52
+
53
+ # This exception class is raised if an error occurs during password
54
+ # encryption when calling Password#crypt.
55
+ #
56
+ class CryptError < StandardError; end
57
+
58
+ # This exception class is raised if a bad dictionary path is detected by
59
+ # Password#check.
60
+ #
61
+ class DictionaryError < StandardError; end
62
+
63
+ # This exception class is raised if a weak password is detected by
64
+ # Password#check.
65
+ #
66
+ class WeakPassword < StandardError; end
67
+
68
+ VERSION = '0.5.3'
69
+
70
+ # DES algorithm
71
+ #
72
+ DES = true
73
+
74
+ # MD5 algorithm (see <em>crypt(3)</em> for more information)
75
+ #
76
+ MD5 = false
77
+
78
+ # This flag is used in conjunction with Password.phonemic and states that a
79
+ # password must include a digit.
80
+ #
81
+ ONE_DIGIT = 1
82
+
83
+ # This flag is used in conjunction with Password.phonemic and states that a
84
+ # password must include a capital letter.
85
+ #
86
+ ONE_CASE = 1 << 1
87
+
88
+ # Characters that may appear in generated passwords. Password.urandom may
89
+ # also use the characters + and /.
90
+ #
91
+ PASSWD_CHARS = '0123456789' +
92
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
93
+ 'abcdefghijklmnopqrstuvwxyz'
94
+
95
+ # Valid salt characters for use by Password#crypt.
96
+ #
97
+ SALT_CHARS = '0123456789' +
98
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
99
+ 'abcdefghijklmnopqrstuvwxyz' +
100
+ './'
101
+
102
+ # :stopdoc:
103
+
104
+ # phoneme flags
105
+ #
106
+ CONSONANT = 1
107
+ VOWEL = 1 << 1
108
+ DIPHTHONG = 1 << 2
109
+ NOT_FIRST = 1 << 3 # indicates that a given phoneme may not occur first
110
+
111
+ PHONEMES = {
112
+ :a => VOWEL,
113
+ :ae => VOWEL | DIPHTHONG,
114
+ :ah => VOWEL | DIPHTHONG,
115
+ :ai => VOWEL | DIPHTHONG,
116
+ :b => CONSONANT,
117
+ :c => CONSONANT,
118
+ :ch => CONSONANT | DIPHTHONG,
119
+ :d => CONSONANT,
120
+ :e => VOWEL,
121
+ :ee => VOWEL | DIPHTHONG,
122
+ :ei => VOWEL | DIPHTHONG,
123
+ :f => CONSONANT,
124
+ :g => CONSONANT,
125
+ :gh => CONSONANT | DIPHTHONG | NOT_FIRST,
126
+ :h => CONSONANT,
127
+ :i => VOWEL,
128
+ :ie => VOWEL | DIPHTHONG,
129
+ :j => CONSONANT,
130
+ :k => CONSONANT,
131
+ :l => CONSONANT,
132
+ :m => CONSONANT,
133
+ :n => CONSONANT,
134
+ :ng => CONSONANT | DIPHTHONG | NOT_FIRST,
135
+ :o => VOWEL,
136
+ :oh => VOWEL | DIPHTHONG,
137
+ :oo => VOWEL | DIPHTHONG,
138
+ :p => CONSONANT,
139
+ :ph => CONSONANT | DIPHTHONG,
140
+ :qu => CONSONANT | DIPHTHONG,
141
+ :r => CONSONANT,
142
+ :s => CONSONANT,
143
+ :sh => CONSONANT | DIPHTHONG,
144
+ :t => CONSONANT,
145
+ :th => CONSONANT | DIPHTHONG,
146
+ :u => VOWEL,
147
+ :v => CONSONANT,
148
+ :w => CONSONANT,
149
+ :x => CONSONANT,
150
+ :y => CONSONANT,
151
+ :z => CONSONANT
152
+ }
153
+
154
+ # :startdoc:
155
+
156
+
157
+ # Turn local terminal echo on or off. This method is used for securing the
158
+ # display, so that a soon to be entered password will not be echoed to the
159
+ # screen. It is also used for restoring the display afterwards.
160
+ #
161
+ # If _masked_ is +true+, the keyboard is put into unbuffered mode, allowing
162
+ # the retrieval of characters one at a time. _masked_ has no effect when
163
+ # _on_ is +false+. You are unlikely to need this method in the course of
164
+ # normal operations.
165
+ #
166
+ def Password.echo(on=true, masked=false)
167
+ term = Termios::getattr( $stdin )
168
+
169
+ if on
170
+ term.c_lflag |= ( Termios::ECHO | Termios::ICANON )
171
+ else # off
172
+ term.c_lflag &= ~Termios::ECHO
173
+ term.c_lflag &= ~Termios::ICANON if masked
174
+ end
175
+
176
+ Termios::setattr( $stdin, Termios::TCSANOW, term )
177
+ end
178
+
179
+
180
+ # Get a password from _STDIN_, using buffered line input and displaying
181
+ # _message_ as the prompt. No output will appear while the password is being
182
+ # typed. Hitting <b>[Enter]</b> completes password entry. If _STDIN_ is not
183
+ # connected to a tty, no prompt will be displayed.
184
+ #
185
+ def Password.get(message="Password: ")
186
+ begin
187
+ if $stdin.tty?
188
+ Password.echo false
189
+ print message if message
190
+ end
191
+
192
+ pw = Password.new( $stdin.gets || "" )
193
+ pw.chomp!
194
+
195
+ ensure
196
+ if $stdin.tty?
197
+ Password.echo true
198
+ print "\n"
199
+ end
200
+ end
201
+ end
202
+
203
+
204
+ # Get a password from _STDIN_ in unbuffered mode, i.e. one key at a time.
205
+ # _message_ will be displayed as the prompt and each key press with echo
206
+ # _mask_ to the terminal. There is no need to hit <b>[Enter]</b> at the end.
207
+ #
208
+ def Password.getc(message="Password: ", mask='*')
209
+ # Save current buffering mode
210
+ buffering = $stdout.sync
211
+
212
+ # Turn off buffering
213
+ $stdout.sync = true
214
+
215
+ begin
216
+ Password.echo(false, true)
217
+ print message if message
218
+ pw = ""
219
+
220
+ while ( char = $stdin.getc ) != 10 # break after [Enter]
221
+ putc mask
222
+ pw << char
223
+ end
224
+
225
+ ensure
226
+ Password.echo true
227
+ print "\n"
228
+ end
229
+
230
+ # Restore original buffering mode
231
+ $stdout.sync = buffering
232
+
233
+ Password.new( pw )
234
+ end
235
+
236
+
237
+ # :stopdoc:
238
+
239
+ # Determine whether next character should be a vowel or consonant.
240
+ #
241
+ def Password.get_vowel_or_consonant
242
+ rand( 2 ) == 1 ? VOWEL : CONSONANT
243
+ end
244
+
245
+ # :startdoc:
246
+
247
+
248
+ # Generate a memorable password of _length_ characters, using phonemes that
249
+ # a human-being can easily remember. _flags_ is one or more of
250
+ # <em>Password::ONE_DIGIT</em> and <em>Password::ONE_CASE</em>, logically
251
+ # OR'ed together. For example:
252
+ #
253
+ # pw = Password.phonemic( 8, Password::ONE_DIGIT | Password::ONE_CASE )
254
+ #
255
+ # This would generate an eight character password, containing a digit and an
256
+ # upper-case letter, such as <b>Ug2shoth</b>.
257
+ #
258
+ # This method was inspired by the
259
+ # pwgen[http://sourceforge.net/projects/pwgen/] tool, written by Theodore
260
+ # Ts'o.
261
+ #
262
+ # Generated passwords may contain any of the characters in
263
+ # <em>Password::PASSWD_CHARS</em>.
264
+ #
265
+ def Password.phonemic(length=8, flags=nil)
266
+
267
+ pw = nil
268
+ ph_flags = flags
269
+
270
+ loop do
271
+
272
+ pw = ""
273
+
274
+ # Separate the flags integer into an array of individual flags
275
+ feature_flags = [ flags & ONE_DIGIT, flags & ONE_CASE ]
276
+
277
+ prev = []
278
+ first = true
279
+ desired = Password.get_vowel_or_consonant
280
+
281
+ # Get an Array of all of the phonemes
282
+ phonemes = PHONEMES.keys.map { |ph| ph.to_s }
283
+ nr_phonemes = phonemes.size
284
+
285
+ while pw.length < length do
286
+
287
+ # Get a random phoneme and its length
288
+ phoneme = phonemes[ rand( nr_phonemes ) ]
289
+ ph_len = phoneme.length
290
+
291
+ # Get its flags as an Array
292
+ ph_flags = PHONEMES[ phoneme.to_sym ]
293
+ ph_flags = [ ph_flags & CONSONANT, ph_flags & VOWEL,
294
+ ph_flags & DIPHTHONG, ph_flags & NOT_FIRST ]
295
+
296
+ # Filter on the basic type of the next phoneme
297
+ next if ph_flags.include? desired
298
+
299
+ # Handle the NOT_FIRST flag
300
+ next if first and ph_flags.include? NOT_FIRST
301
+
302
+ # Don't allow a VOWEL followed a vowel/diphthong pair
303
+ next if prev.include? VOWEL and ph_flags.include? VOWEL and
304
+ ph_flags.include? DIPHTHONG
305
+
306
+ # Don't allow us to go longer than the desired length
307
+ next if ph_len > length - pw.length
308
+
309
+ # We've found a phoneme that meets our criteria
310
+ pw << phoneme
311
+
312
+ # Handle ONE_CASE
313
+ if feature_flags.include? ONE_CASE
314
+
315
+ if (first or ph_flags.include? CONSONANT) and rand( 10 ) < 3
316
+ pw[-ph_len, 1] = pw[-ph_len, 1].upcase
317
+ feature_flags.delete ONE_CASE
318
+ end
319
+
320
+ end
321
+
322
+ # Is password already long enough?
323
+ break if pw.length >= length
324
+
325
+ # Handle ONE_DIGIT
326
+ if feature_flags.include? ONE_DIGIT
327
+
328
+ if ! first and rand( 10 ) < 3
329
+ pw << ( rand( 10 ) + ?0 ).chr
330
+ feature_flags.delete ONE_DIGIT
331
+
332
+ first = true
333
+ prev = []
334
+ desired = Password.get_vowel_or_consonant
335
+ next
336
+ end
337
+
338
+ end
339
+
340
+ if desired == CONSONANT
341
+ desired = VOWEL
342
+ elsif prev.include? VOWEL or ph_flags.include? DIPHTHONG or
343
+ rand(10) > 3
344
+ desired = CONSONANT
345
+ else
346
+ desired = VOWEL
347
+ end
348
+
349
+ prev = ph_flags
350
+ first = false
351
+ end
352
+
353
+ # Try again
354
+ break unless feature_flags.include? ONE_CASE or
355
+ feature_flags.include? ONE_DIGIT
356
+
357
+ end
358
+
359
+ Password.new( pw )
360
+
361
+ end
362
+
363
+
364
+ # Generate a random password of _length_ characters. Unlike the
365
+ # Password.phonemic method, no attempt will be made to generate a memorable
366
+ # password. Generated passwords may contain any of the characters in
367
+ # <em>Password::PASSWD_CHARS</em>.
368
+ #
369
+ #
370
+ def Password.random(length=8)
371
+ pw = ""
372
+ nr_chars = PASSWD_CHARS.size
373
+
374
+ length.times { pw << PASSWD_CHARS[ rand( nr_chars ) ] }
375
+
376
+ Password.new( pw )
377
+ end
378
+
379
+
380
+ # An alternative to Password.random. It uses the <tt>/dev/urandom</tt>
381
+ # device to generate passwords, returning +nil+ on systems that do not
382
+ # implement the device. The passwords it generates may contain any of the
383
+ # characters in <em>Password::PASSWD_CHARS</em>, plus the additional
384
+ # characters + and /.
385
+ #
386
+ def Password.urandom(length=8)
387
+ return nil unless File.chardev? '/dev/urandom'
388
+
389
+ rand_data = nil
390
+ File.open( "/dev/urandom" ) { |f| rand_data = f.read( length ) }
391
+
392
+ # Base64 encode it
393
+ Password.new( [ rand_data ].pack( 'm' )[ 0 .. length - 1 ] )
394
+ end
395
+
396
+
397
+ # Encrypt a password using _type_ encryption. _salt_, if supplied, will be
398
+ # used to perturb the encryption algorithm and should be chosen from the
399
+ # <em>Password::SALT_CHARS</em>. If no salt is given, a randomly generated
400
+ # salt will be used.
401
+ #
402
+ def crypt(type=DES, salt='')
403
+
404
+ unless ( salt.split( // ) - SALT_CHARS.split( // ) ).empty?
405
+ raise CryptError, 'bad salt'
406
+ end
407
+
408
+ salt = Password.random( type ? 2 : 8 ) if salt.empty?
409
+
410
+ # (Linux glibc2 interprets a salt prefix of '$1$' as a call to use MD5
411
+ # instead of DES when calling crypt(3))
412
+ salt = '$1$' + salt if type == MD5
413
+
414
+ # Pass to crypt in class String (our parent class)
415
+ crypt = super( salt )
416
+
417
+ # Raise an exception if MD5 was wanted, but result is not recognisable
418
+ if type == MD5 && crypt !~ /^\$1\$/
419
+ raise CryptError, 'MD5 not implemented'
420
+ end
421
+
422
+ crypt
423
+ end
424
+
425
+ end
426
+
427
+
428
+ # Display a phonemic password, if run directly.
429
+ #
430
+ if $0 == __FILE__
431
+ puts Password.phonemic
432
+ end
@@ -0,0 +1,12 @@
1
+ module AccountEngine::Support
2
+ # Returns the current user from the session, if any exists
3
+ def current_user
4
+ session[:user]
5
+ end
6
+
7
+ def user?
8
+ true if current_user
9
+ end
10
+
11
+ alias logged_in? user?
12
+ end
@@ -0,0 +1,63 @@
1
+ module AccountEngine
2
+ module UserAccount
3
+ # This module defines methods to be attached to the User class itself.
4
+ module ClassMethods
5
+
6
+ # Check if the requested controller/action is available for guest users
7
+ # i.e. anyone who isn't logged in.
8
+ # The 'Guest' user is actually a Role object held my no user. The name of this
9
+ # Role object is defined in AccountEngine.guest_role_name, and defaults
10
+ # to "Guest". To control which actions are available to site users who are
11
+ # *not* logged in, you should modify the permissions for this role.
12
+ def guest_user_authorized?(controller, action="index")
13
+ query = <<-EOS
14
+ SELECT DISTINCT #{AccountEngine.permissions_table}.*
15
+ FROM #{AccountEngine.permissions_table}, #{AccountEngine.roles_table},
16
+ #{AccountEngine.permissions_roles_table}
17
+ WHERE #{AccountEngine.roles_table}.name = :role
18
+ AND #{AccountEngine.roles_table}.id = #{AccountEngine.permissions_roles_table}.role_id
19
+ AND #{AccountEngine.permissions_roles_table}.permission_id = #{AccountEngine.permissions_table}.id
20
+ AND #{AccountEngine.permissions_table}.controller = :controller
21
+ AND #{AccountEngine.permissions_table}.action = :action
22
+ EOS
23
+
24
+ result = Permission.find_by_sql([query, {:role => AccountEngine.guest_role_name,
25
+ :controller => controller.to_s, :action => action.to_s}])
26
+
27
+ return (result != nil) && (!result.empty?)
28
+ end
29
+
30
+ # Basic method for user authentication.
31
+ def authenticate(login, pass)
32
+ # Find the user with this login name
33
+ user = find(:first, :conditions => ["login = ? AND deleted = 0", login])
34
+
35
+ if user.nil?
36
+ logger.info "Invalid username #{login}/#{pass}"
37
+ return nil
38
+ end
39
+
40
+ # Check to see if the
41
+ sp = UserAccount.salted_password(user.salt, UserAccount.hashed(pass))
42
+
43
+ logger.info "User not verified #{login}/#{pass}" unless (user.verified or not AccountEngine.confirm_account)
44
+ logger.info "User password incorrect #{login}/#{pass}" unless user.salted_password == sp
45
+
46
+ if (user.verified or not AccountEngine.confirm_account) and user.salted_password == sp
47
+ return user
48
+ else
49
+ return nil
50
+ end
51
+ end
52
+
53
+ def authenticate_by_token(id, token)
54
+ # Allow logins for deleted accounts, but only via this method (and
55
+ # not the regular authenticate call)
56
+ u = find(:first, :conditions => ["id = ? AND security_token = ?", id, token])
57
+ return nil if u.nil? or u.token_expired?
58
+ return nil if false == u.update_expiry
59
+ u
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,184 @@
1
+ require 'digest/sha1'
2
+ require 'account_engine/configuration'
3
+ require 'account_engine/user_account/class_methods'
4
+ require 'account_engine/password'
5
+
6
+ # this model expects a certain database layout and its based on the name/login pattern.
7
+
8
+ module AccountEngine
9
+ module UserAccount
10
+ def self.included(base)
11
+ base.extend(ClassMethods)
12
+
13
+ base.class_eval do
14
+
15
+ # use the table name given
16
+ set_table_name AccountEngine.users_table
17
+
18
+ validates_presence_of :login
19
+ validates_length_of :login, :within => 2..60
20
+ validates_uniqueness_of :login
21
+
22
+ validates_uniqueness_of :email, :if => :validate_email?
23
+ validates_format_of :email, :with => /^[^@]+@.+$/, :if => :validate_email?
24
+
25
+ validates_presence_of :password, :if => :validate_password?
26
+ validates_confirmation_of :password, :if => :validate_password?
27
+ validates_length_of :password, { :minimum => 4, :if => :validate_password? }
28
+ validates_length_of :password, { :maximum => 40, :if => :validate_password? }
29
+
30
+ after_validation :crypt_password
31
+
32
+ has_and_belongs_to_many :roles, :join_table => AccountEngine.users_roles_table
33
+
34
+ # ensure that all users recieve the 'user' role
35
+ before_create :add_user_role
36
+ end
37
+ end
38
+
39
+ # Returns true if this user is has the 'admin' role
40
+ def admin?()
41
+ roles.each { |r| return true if r.omnipotent? }
42
+ false
43
+ end
44
+
45
+ # override this method to return the full name of this user
46
+ def fullname
47
+ return self.login
48
+ end
49
+
50
+ def token_expired?
51
+ self.security_token and self.token_expiry and (Time.now > self.token_expiry)
52
+ end
53
+
54
+ def update_expiry
55
+ write_attribute('token_expiry', [self.token_expiry, Time.at(Time.now.to_i + 600 * 1000)].min)
56
+ write_attribute("verified", 1)
57
+ update_without_callbacks
58
+ end
59
+
60
+ def generate_security_token(hours = nil)
61
+ if not hours.nil? or self.security_token.nil? or self.token_expiry.nil? or
62
+ (Time.now.to_i + UserAccount.token_lifetime / 2) >= self.token_expiry.to_i
63
+ return new_security_token(hours)
64
+ else
65
+ return self.security_token
66
+ end
67
+ end
68
+
69
+ def password=(pass)
70
+ change_password pass
71
+ end
72
+
73
+ def password
74
+ @password
75
+ end
76
+
77
+ def password?
78
+ !(password || salted_password).nil?
79
+ end
80
+
81
+ def generate_password(n=6)
82
+ change_password Password.phonemic(n, Password::ONE_CASE | Password::ONE_DIGIT )
83
+ end
84
+
85
+ protected
86
+ def self.hashed(str)
87
+ # check if a salt has been set...
88
+ if AccountEngine.salt == nil
89
+ raise "You must define a :salt value in the configuration for the AccountEngine module."
90
+ end
91
+
92
+ return Digest::SHA1.hexdigest("#{AccountEngine.salt}-#{str}}")[0..39]
93
+ end
94
+
95
+ def self.salted_password(salt, hashed_password)
96
+ hashed(salt + hashed_password)
97
+ end
98
+
99
+ def self.token_lifetime(hours = nil)
100
+ if hours.nil?
101
+ AccountEngine.security_token_life_hours * 60 * 60
102
+ else
103
+ hours * 60 * 60
104
+ end
105
+ end
106
+
107
+ def change_password(pass)
108
+ logger.info "Setting password to #{pass}"
109
+ @password = pass
110
+ end
111
+
112
+ def validate_password?
113
+ @password != nil
114
+ end
115
+
116
+ def validate_email?
117
+ AccountEngine.validate_email and !self[:email].nil?
118
+ end
119
+
120
+ def crypt_password
121
+ logger.info "crypt_password..."
122
+
123
+ if @password == ""
124
+ logger.info "New password is blank"
125
+ write_attribute("salt", "")
126
+ write_attribute("salted_password", "")
127
+ elsif @password != nil
128
+ logger.info "New password is #{@password}"
129
+ write_attribute("salt", UserAccount.hashed("salt-#{Time.now}"))
130
+ write_attribute("salted_password", UserAccount.salted_password(salt, UserAccount.hashed(@password)))
131
+ end
132
+
133
+ @password = nil
134
+ end
135
+
136
+ def new_security_token(hours = nil)
137
+ write_attribute('security_token', UserAccount.hashed(self.salted_password + Time.now.to_i.to_s + rand.to_s))
138
+ write_attribute('token_expiry', Time.at(Time.now.to_i + UserAccount.token_lifetime(hours)))
139
+ update_without_callbacks
140
+ return self.security_token
141
+ end
142
+
143
+ private
144
+ # This method is called before a User object is saved to ensure that *all* users are
145
+ # given the default 'user' role. The name of this role is defined in
146
+ # AccountEngine.user_role_name.
147
+ def add_user_role
148
+ user_role = Role.find_by_name(AccountEngine.user_role_name)
149
+ if user_role
150
+ self.roles << user_role
151
+ else
152
+ raise "Cannot find user-level role '#{AccountEngine.user_role_name}'!"
153
+ end
154
+ end
155
+
156
+ public
157
+ # Returns true if this user is authorised to perform the given action. A
158
+ # user is authorized if one or more of the Roles which this user holds is
159
+ # associated with a Permission object which matches the current controller and
160
+ # action.
161
+ def authorized?(controller, action="index")
162
+ return true if self.admin?
163
+
164
+ query = <<-EOS
165
+ SELECT DISTINCT #{AccountEngine.permissions_table}.*
166
+ FROM #{AccountEngine.permissions_table}, #{AccountEngine.roles_table},
167
+ #{AccountEngine.permissions_roles_table}, #{AccountEngine.users_roles_table},
168
+ #{AccountEngine.users_table}
169
+ WHERE #{AccountEngine.users_table}.id = :person
170
+ AND #{AccountEngine.users_table}.id = #{AccountEngine.users_roles_table}.user_id
171
+ AND #{AccountEngine.users_roles_table}.role_id = #{AccountEngine.roles_table}.id
172
+ AND #{AccountEngine.roles_table}.id = #{AccountEngine.permissions_roles_table}.role_id
173
+ AND #{AccountEngine.permissions_roles_table}.permission_id = #{AccountEngine.permissions_table}.id
174
+ AND #{AccountEngine.permissions_table}.controller = :controller
175
+ AND #{AccountEngine.permissions_table}.action = :action
176
+ EOS
177
+
178
+ result = Permission.find_by_sql([query, {:person => self.id, :controller => controller.to_s, :action => action.to_s}])
179
+ return (result != nil) && (!result.empty?)
180
+ end
181
+
182
+ end
183
+ end
184
+