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.
- data/README +0 -0
- data/lib/account_engine/configuration.rb +101 -0
- data/lib/account_engine/controller.rb +246 -0
- data/lib/account_engine/helper.rb +104 -0
- data/lib/account_engine/password.rb +432 -0
- data/lib/account_engine/support.rb +12 -0
- data/lib/account_engine/user_account/class_methods.rb +63 -0
- data/lib/account_engine/user_account.rb +184 -0
- data/lib/account_engine.rb +63 -0
- data/rails/app/controllers/account_controller.rb +162 -0
- data/rails/app/controllers/permissions_controller.rb +90 -0
- data/rails/app/controllers/roles_controller.rb +133 -0
- data/rails/app/controllers/users_controller.rb +144 -0
- data/rails/app/helpers/account_helper.rb +3 -0
- data/rails/app/helpers/permissions_helper.rb +3 -0
- data/rails/app/helpers/roles_helper.rb +3 -0
- data/rails/app/helpers/users_helper.rb +3 -0
- data/rails/app/models/permission.rb +129 -0
- data/rails/app/models/role.rb +60 -0
- data/rails/app/models/user.rb +5 -0
- data/rails/app/models/user_notify.rb +75 -0
- data/rails/app/views/account/_form.rhtml +8 -0
- data/rails/app/views/account/change_password.rhtml +17 -0
- data/rails/app/views/account/edit.rhtml +5 -0
- data/rails/app/views/account/forgot_password.rhtml +12 -0
- data/rails/app/views/account/home.rhtml +3 -0
- data/rails/app/views/account/login.rhtml +27 -0
- data/rails/app/views/account/logout.rhtml +8 -0
- data/rails/app/views/account/signup.rhtml +28 -0
- data/rails/app/views/permissions/_form.rhtml +14 -0
- data/rails/app/views/permissions/_list.rhtml +38 -0
- data/rails/app/views/permissions/edit.rhtml +5 -0
- data/rails/app/views/permissions/index.rhtml +3 -0
- data/rails/app/views/permissions/new.rhtml +5 -0
- data/rails/app/views/roles/_form.rhtml +8 -0
- data/rails/app/views/roles/_permissions.rhtml +25 -0
- data/rails/app/views/roles/edit.rhtml +5 -0
- data/rails/app/views/roles/index.rhtml +34 -0
- data/rails/app/views/roles/new.rhtml +5 -0
- data/rails/app/views/roles/show.rhtml +20 -0
- data/rails/app/views/user_notify/change_password.rhtml +10 -0
- data/rails/app/views/user_notify/delete.rhtml +5 -0
- data/rails/app/views/user_notify/forgot_password.rhtml +11 -0
- data/rails/app/views/user_notify/pending_delete.rhtml +9 -0
- data/rails/app/views/user_notify/signup.rhtml +12 -0
- data/rails/app/views/users/_form.rhtml +12 -0
- data/rails/app/views/users/edit.rhtml +5 -0
- data/rails/app/views/users/index.rhtml +38 -0
- data/rails/app/views/users/new.rhtml +5 -0
- data/rails/app/views/users/roles.rhtml +42 -0
- data/rails/app/views/users/show.rhtml +36 -0
- data/rails/assets/images/default/omnipotent.png +0 -0
- data/rails/assets/images/default/system.png +0 -0
- data/rails/assets/images/permissions/create.png +0 -0
- data/rails/assets/images/permissions/sync.png +0 -0
- data/rails/assets/images/roles/add_permission.png +0 -0
- data/rails/assets/images/roles/create.png +0 -0
- data/rails/assets/images/roles/edit.png +0 -0
- data/rails/assets/images/roles/remove_permission.png +0 -0
- data/rails/assets/images/roles/user.png +0 -0
- data/rails/assets/images/table_background.png +0 -0
- data/rails/assets/images/users/create.png +0 -0
- data/rails/assets/images/users/destroy.png +0 -0
- data/rails/assets/images/users/edit.png +0 -0
- data/rails/assets/images/users/show.png +0 -0
- data/rails/assets/javascripts/account_engine.js +166 -0
- data/rails/assets/stylesheets/account_engine.css +7 -0
- data/rails/assets/stylesheets/check_password.css +10 -0
- data/rails/assets/stylesheets/simple.css +168 -0
- data/rails/db/migrate/001_initial_schema.rb +49 -0
- data/rails/init.rb +21 -0
- data/rails/routes.rb +5 -0
- data/rails/tasks/account_engine.rake +123 -0
- 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,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
|
+
|