Changesets can be listed by changeset number.
The Git repository is here.
- Revision:
- 107
- Log:
Further updates to the HubSsoLib Gem to support cross-application,
same domain single sign-on.
- Author:
- adh
- Date:
- Mon Oct 23 18:12:59 +0100 2006
- Size:
- 37955 Bytes
1 | ####################################################################### |
2 | # Module: HubSsoLib # |
3 | # By Hipposoft, 2006 # |
4 | # # |
5 | # Purpose: Cross-application same domain single sign-on support. # |
6 | # # |
7 | # Author: A.D.Hodgkinson # |
8 | # # |
9 | # History: 20-Oct-2006 (ADH): First version of stand-alone library, # |
10 | # split from Hub application. # |
11 | ####################################################################### |
12 | |
13 | # Location of Hub application root. |
14 | HUB_PATH_PREFIX = '/rails/hub' |
15 | |
16 | # Time limit, *in seconds*, for the account inactivity timeout. |
17 | # If a user performs no Clubhouse actions during this time they |
18 | # will be automatically logged out upon their next action. |
19 | HUBSSOLIB_IDLE_TIME_LIMIT = 15 * 60 |
20 | |
21 | # Random file location. |
22 | HUBSSOLIB_RND_FILE_PATH = '/home/adh/.rnd' |
23 | |
24 | # Session data cookie name. |
25 | HUBSSOLIB_SESSION_DATA_KEY = 'hubapp_session_data' |
26 | |
27 | # Session support cookie name. |
28 | HUBSSOLIB_SESSION_SUPPORT_KEY = 'hubapp_session_support' |
29 | |
30 | module HubSsoLib |
31 | |
32 | ####################################################################### |
33 | # Class: Crypto # |
34 | # By Hipposoft, 2006 # |
35 | # # |
36 | # Purpose: Encryption and decryption utilities. # |
37 | # # |
38 | # Author: A.D.Hodgkinson # |
39 | # # |
40 | # History: 28-Aug-2006 (ADH): First version. # |
41 | # 20-Oct-2006 (ADH): Integrated into HubSsoLib, renamed to # |
42 | # 'Crypto' from 'HubSsoCrypto'. # |
43 | ####################################################################### |
44 | |
45 | # Encryption and decryption utility object. Once instantiated with the |
46 | # filename of a file that holds at least 1K of pseudo-random data, a |
47 | # HubSsoLib::Crypto object is used to encrypt and decrypt data with the |
48 | # AES-256-CBC cipher. A single passphrase is used for both operations. |
49 | # A SHA-256 hash of that passphrase is used as the encryption key. |
50 | # |
51 | # CBC operation requires an initialization vector for the first block of |
52 | # data during encryption and decryption. The file of random data is used |
53 | # for this in conjunction with the passphrase used to generate the key. By |
54 | # so doing, the initialization vector is not revealed to third parties, |
55 | # even though the source code of the object is available. The weakness is |
56 | # that for a given passphrase and random data pool the same initialization |
57 | # vector will always be generated - indeed, this is relied upon, to allow |
58 | # callers themselves to only have to remember the passphrase. See private |
59 | # method obtain_iv() for more details. |
60 | # |
61 | class Crypto |
62 | |
63 | require 'openssl' |
64 | require 'digest/sha2' |
65 | require 'digest/md5' |
66 | |
67 | # Initialize the HubSsoLib::Crypto object. Must pass a pathname to a file |
68 | # of effectively random data of at least 1K in length. If the data is |
69 | # larger than 16K in size, everything after the first 16K will be |
70 | # ignored. The data is cached internally when the object starts. |
71 | # |
72 | def initialize(rnd_file) |
73 | # Check the file size and find out how much data to read - at least 1K, |
74 | # no more than 16K. Store the size in @rnd_size and read the data into |
75 | # @rnd_data, both for use later. |
76 | |
77 | @rnd_size = File.size(rnd_file) |
78 | @rnd_size = 16384 if (@rnd_size > 16384) |
79 | |
80 | if @rnd_size < 1024 |
81 | raise "HubSsoLib::Crypto objects need at least 1024 bytes of random data - file '#{rnd_file}' is too small" |
82 | else |
83 | @rnd_data = File.open(rnd_file, 'rb').read(@rnd_size) |
84 | end |
85 | end |
86 | |
87 | # Encrypt the given data with the AES-256-CBC algorithm using the |
88 | # given passphrase. Returns the encrypted result in a string. |
89 | # Distantly based upon: |
90 | # |
91 | # http://www.bigbold.com/snippets/posts/show/576 |
92 | # |
93 | def encrypt(data, passphrase) |
94 | cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc") |
95 | cipher.encrypt |
96 | |
97 | cipher.key = Digest::SHA256.digest(passphrase) |
98 | cipher.iv = obtain_iv(passphrase) |
99 | |
100 | encrypted = cipher.update(data) |
101 | encrypted << cipher.final |
102 | |
103 | return encrypted |
104 | end |
105 | |
106 | # Decrypt the given data with the AES-256-CBC algorithm using the |
107 | # given passphrase. Returns 'nil' if there is any kind of error in |
108 | # the decryption process. Distantly based upon: |
109 | # |
110 | # http://www.bigbold.com/snippets/posts/show/576 |
111 | # |
112 | def decrypt(data, passphrase) |
113 | cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc") |
114 | cipher.decrypt |
115 | |
116 | cipher.key = Digest::SHA256.digest(passphrase) |
117 | cipher.iv = obtain_iv(passphrase) |
118 | |
119 | decrypted = cipher.update(data) |
120 | decrypted << cipher.final |
121 | |
122 | return decrypted |
123 | rescue |
124 | return nil |
125 | end |
126 | |
127 | # Encode some given data in base-64 format with no line breaks. |
128 | # |
129 | def pack64(data) |
130 | [data].pack('m1000000') # Stupid long number to avoid "\n" in the output |
131 | end |
132 | |
133 | # Decode some given data from base-64 format with no line breaks. |
134 | # |
135 | def unpack64(data) |
136 | data.unpack('m').first |
137 | end |
138 | |
139 | # Encrypt and base-64 encode the given data with the given passphrase. |
140 | # Returns the encoded result. |
141 | # |
142 | def encode(data, passphrase) |
143 | pack64(encrypt(data, passphrase)) |
144 | end |
145 | |
146 | # Decrypt and base-64 decode the given data with the given passphrase. |
147 | # Returns the decoded result or 'nil' on error. |
148 | # |
149 | def decode(data, passphrase) |
150 | decrypt(unpack64(data), passphrase) |
151 | rescue |
152 | return nil |
153 | end |
154 | |
155 | # "Scramble" a passphrase. Cookie data encryption is done purely so that |
156 | # some hypothetical malicious user cannot easily examine or modify the |
157 | # cookie contents for some nefarious purpose. Encryption is done at the |
158 | # head end. We need to be able to decrypt in the absence of any other |
159 | # information. A fixed passphrase thus needs to be used, but it cannot be |
160 | # included in the source code or anyone can read the cookie contents! To |
161 | # work around this, transform the passphrase into 32 bytes of data from |
162 | # the random pool if asked. The random pool is not known to the outside |
163 | # world so security is improved (albeit far from perfect, but this is all |
164 | # part of little more than an anti-spam measure - not Fort Knox!). |
165 | # |
166 | def scramble_passphrase(passphrase) |
167 | |
168 | # Generate a 16-byte hash of the passphrase using the MD5 algorithm. Get |
169 | # this as a string of hex digits and convert that into an integer. Strip |
170 | # off the top bits (since we've no more reason to believe that the top |
171 | # bits contain more randomly varying data than the bottom bits) so that |
172 | # the number is bound to between zero and the random pool size, minus |
173 | # 33, thus providing an offset into the file from which we can safely |
174 | # read 32 bytes of data. |
175 | |
176 | offset = Digest::MD5.hexdigest(passphrase).hex % (@rnd_size - 32) |
177 | |
178 | # Return 32 bytes of data from the random pool at the calculated offset. |
179 | |
180 | return @rnd_data[offset..offset + 31] |
181 | end |
182 | |
183 | private |
184 | |
185 | # Obtain an initialization vector (IV) of 32 bytes (256 bits) length based |
186 | # on external data loaded when the object was created. Since the data |
187 | # content is unknown, the IV is unknown. This is important; see: |
188 | # |
189 | # http://www.ciphersbyritter.com/GLOSSARY.HTM#CipherBlockChaining |
190 | # |
191 | # Weakness: An offset into the supplied data is generated from the given |
192 | # passphrase. Since the data is cached internally, the same IV will be |
193 | # produced for any given passphrase (this is as much a feature as it is a |
194 | # weakness, since the encryption and decryption routines rely on it). |
195 | # |
196 | # The passphrase scrambler is used to do the back-end work. Since the |
197 | # caller may have already scrambled the passphrase once, scrambled data is |
198 | # used as input; we end up scrambling it twice. This is a desired result - |
199 | # we don't want the IV being the data that's actually also used for the |
200 | # encryption passphrase. |
201 | # |
202 | def obtain_iv(passphrase) |
203 | return scramble_passphrase(passphrase) |
204 | end |
205 | |
206 | end # Crypto class |
207 | |
208 | ####################################################################### |
209 | # Class: Roles # |
210 | # By Hipposoft, 2006 # |
211 | # # |
212 | # Purpose: Shared methods for handling user account roles. # |
213 | # # |
214 | # Author: A.D.Hodgkinson # |
215 | # # |
216 | # History: 17-Oct-2006 (ADH): Adapted from Clubhouse. # |
217 | # 20-Oct-2006 (ADH): Integrated into HubSsoLib. # |
218 | ####################################################################### |
219 | |
220 | class Roles |
221 | |
222 | # Association of symbolic role names to display names, in no |
223 | # particular order. |
224 | # |
225 | ROLES = { |
226 | :admin => 'Administrator', |
227 | :webmaster => 'Webmaster', |
228 | :privileged => 'Advanced user', |
229 | :normal => 'Normal user' |
230 | } |
231 | |
232 | ADMIN = :admin |
233 | NORMAL = :normal |
234 | |
235 | # Return the display name of a given role symbol. Class method. |
236 | # |
237 | def self.get_display_name(symbol) |
238 | ROLES[symbol] |
239 | end |
240 | |
241 | # Return all display names in an array. Class method. |
242 | |
243 | def self.get_display_names |
244 | ROLES.values |
245 | end |
246 | |
247 | # Return an array of known role symbols. They can be used with |
248 | # methods like get_display_name. Class method. |
249 | |
250 | def self.get_role_symbols |
251 | ROLES.keys |
252 | end |
253 | |
254 | # Initialize a new Roles object. Pass 'true' if this is for |
255 | # an admin user account, else 'false'. Default is 'false'. Note |
256 | # that further down in this file, the String, Symbol and Array |
257 | # classes are extended with to_authenticated_roles methods, which |
258 | # provide other ways of creating Roles objects. |
259 | # |
260 | def initialize(admin = false) |
261 | if (admin) |
262 | @role_array = [ ADMIN ] |
263 | else |
264 | @role_array = [ NORMAL ] |
265 | end |
266 | end |
267 | |
268 | # Adds a role, supplied as a string or symbol, to the internal list. |
269 | # A non-nil return indicates that the role was already present. |
270 | # |
271 | def add(role) |
272 | @role_array.push(role.to_s.intern).uniq! |
273 | end |
274 | |
275 | # Deletes a role, supplied as a string or symbol, from the internal |
276 | # list. A nil return indicates that the role was not in the list. |
277 | # |
278 | def delete(role) |
279 | @role_array.delete(role.to_s.intern) |
280 | end |
281 | |
282 | # Delete all roles from the internal list. |
283 | # |
284 | def clear |
285 | @role_array.clear |
286 | end |
287 | |
288 | # Return a copy of the internal roles list as a string. |
289 | # |
290 | def to_s |
291 | return @role_array.join(',') |
292 | end |
293 | |
294 | # Return a copy of the internal roles list as an array. |
295 | # |
296 | def to_a |
297 | return @role_array.dup |
298 | end |
299 | |
300 | # Return a copy of the intenal roles list as a human readable string. |
301 | # |
302 | def to_human_s |
303 | human_names = [] |
304 | |
305 | @role_array.each do |role| |
306 | human_names.push(HubSsoLib::Roles.get_display_name(role)) |
307 | end |
308 | |
309 | if (human_names.length == 0) |
310 | return '' |
311 | elsif (human_names.length == 1) |
312 | return human_names[0] |
313 | else |
314 | return human_names[0..-2].join(', ') + ' and ' + human_names.last |
315 | end |
316 | end |
317 | |
318 | # Do nothing - this is just useful for polymorphic code, where a function |
319 | # can take a String, Array, Symbol or Roles object and make the |
320 | # same method call to return a Roles object in return. |
321 | # |
322 | def to_authenticated_roles |
323 | return self |
324 | end |
325 | |
326 | # Does the internal list of roles include the supplied role or roles? |
327 | # The roles can be given as an array of individual role symbols or |
328 | # equivalent strings, or as a single symbol or single equivalent |
329 | # symbol, or as a string containing equivalents of role symbols in a |
330 | # comma-separated list (no white space or other spurious characters). |
331 | # Returns 'true' if the internal list of roles includes at least one |
332 | # of the supplied roles, else 'false'. |
333 | # |
334 | def include?(roles) |
335 | return false if roles.nil? |
336 | |
337 | # Ensure we've an array of roles, one way or another |
338 | roles = roles.to_s if roles.class == Symbol |
339 | roles = roles.split(',') if roles.class == String |
340 | |
341 | roles.each do |role| |
342 | return true if @role_array.include?(role.to_s.intern) |
343 | end |
344 | |
345 | return false |
346 | end |
347 | |
348 | # Synonym for 'include?'. |
349 | # |
350 | alias includes? include? |
351 | |
352 | # Validate the list of roles. Validation means ensuring that all |
353 | # roles in this object are found in the internal ROLES hash. Returns |
354 | # true if the roles validate or false if unknown roles are found. |
355 | # |
356 | def validate |
357 | return false if @role_array.empty? |
358 | |
359 | @role_array.each do |role| |
360 | return false unless ROLES[role] |
361 | end |
362 | |
363 | return true |
364 | end |
365 | |
366 | end # Roles class |
367 | |
368 | ####################################################################### |
369 | # Class: Permissions # |
370 | # By Hipposoft, 2006 # |
371 | # # |
372 | # Purpose: Methods to help, in conjunction with Roles, determine the # |
373 | # access permissions a particular user is granted. # |
374 | # # |
375 | # Author: A.D.Hodgkinson # |
376 | # # |
377 | # History: 17-Oct-2006 (ADH): Adapted from Clubhouse. # |
378 | # 20-Oct-2006 (ADH): Integrated into HubSsoLib. # |
379 | ####################################################################### |
380 | |
381 | class Permissions |
382 | |
383 | # Initialize a permissions object. The map is a hash which maps action |
384 | # names, expressed as symbols, to roles, expressed as individual symbols, |
385 | # equivalent strings, or arrays of multiple strings or symbols. Use 'nil' |
386 | # to indicate permission for the general public - no login required - or |
387 | # simply omit the action (unlisted actions are permitted). |
388 | # |
389 | # Example mapping for a generic controller: |
390 | # |
391 | # { |
392 | # :new => [ :admin, :webmaster, :privileged, :normal ], |
393 | # :create => [ :admin, :webmaster, :privileged, :normal ], |
394 | # :edit => [ :admin, :webmaster, :privileged, :normal ], |
395 | # :update => [ :admin, :webmaster, :privileged, :normal ], |
396 | # :delete => [ :admin, :webmaster, :privileged ], |
397 | # :list => nil, |
398 | # :show => nil |
399 | # } |
400 | # |
401 | def initialize(pmap) |
402 | @permissions = pmap |
403 | end |
404 | |
405 | # Does the given Roles object grant permission for the given action, |
406 | # expressed as a string or symbol? Returns 'true' if so, else 'false'. |
407 | # |
408 | # If a role is given as some other type, an attempt is made to convert |
409 | # it to a Roles object internally (so you could pass a role symbol, |
410 | # string, array of symbols or strings, or comma-separated string). |
411 | # |
412 | # Passing an empty roles string will tell you whether or not the |
413 | # action requires login. Only actions not in the permissions list or |
414 | # those with a 'nil' list of roles will generate a result 'true', |
415 | # since any other actions will require your empty roles string to |
416 | # include at least one role (which it obviously doesn't). |
417 | # |
418 | def permitted?(roles, action) |
419 | action = action.to_s.intern |
420 | roles = roles.to_authenticated_roles |
421 | |
422 | return true unless @permissions.include?(action) |
423 | return true if @permissions[action].nil? |
424 | return roles.include?(@permissions[action]) |
425 | end |
426 | end # Permissions class |
427 | |
428 | ####################################################################### |
429 | # Module: User # |
430 | # By Hipposoft, 2006 # |
431 | # # |
432 | # Purpose: A representation of the Hub application's User Model in # |
433 | # terms of a simple set of properties, so that applications # |
434 | # don't need User access to understand user attributes. # |
435 | # # |
436 | # Author: A.D.Hodgkinson # |
437 | # # |
438 | # History: 21-Oct-2006 (ADH): Created. # |
439 | ####################################################################### |
440 | |
441 | class User |
442 | attr_accessor :salt |
443 | attr_accessor :roles |
444 | attr_accessor :updated_at |
445 | attr_accessor :activated_at |
446 | attr_accessor :real_name |
447 | attr_accessor :crypted_password |
448 | attr_accessor :remember_token_expires_at |
449 | attr_accessor :activation_code |
450 | attr_accessor :member_id |
451 | attr_accessor :id |
452 | attr_accessor :password_reset_code |
453 | attr_accessor :remember_token |
454 | attr_accessor :email |
455 | attr_accessor :created_at |
456 | attr_accessor :password_reset_code_expires_at |
457 | |
458 | def initialize |
459 | self.salt = nil |
460 | self.roles = nil |
461 | self.updated_at = nil |
462 | self.activated_at = nil |
463 | self.real_name = nil |
464 | self.crypted_password = nil |
465 | self.remember_token_expires_at = nil |
466 | self.activation_code = nil |
467 | self.member_id = nil |
468 | self.id = nil |
469 | self.password_reset_code = nil |
470 | self.remember_token = nil |
471 | self.email = nil |
472 | self.created_at = nil |
473 | self.password_reset_code_expires_at = nil |
474 | end |
475 | end |
476 | |
477 | ####################################################################### |
478 | # Module: SessionData # |
479 | # By Hipposoft, 2006 # |
480 | # # |
481 | # Purpose: Session support object, used to store session metadata in # |
482 | # an insecure cross-application cookie. # |
483 | # # |
484 | # Author: A.D.Hodgkinson # |
485 | # # |
486 | # History: 22-Oct-2006 (ADH): Created. # |
487 | ####################################################################### |
488 | |
489 | class SessionData |
490 | attr_accessor :last_used |
491 | attr_accessor :return_to |
492 | attr_accessor :flash |
493 | |
494 | def initialize |
495 | self.last_used = Time.now.utc |
496 | self.return_to = nil |
497 | self.flash = {} |
498 | end |
499 | end |
500 | |
501 | ####################################################################### |
502 | # Module: Core # |
503 | # Various authors # |
504 | # # |
505 | # Purpose: The core of acts_as_authenticated's authorisation # |
506 | # functions (its AuthenticatedSystem module), modified to # |
507 | # work with the other parts of HubSsoLib. You should include # |
508 | # this module to use its facilities. # |
509 | # # |
510 | # Author: Various; adaptation by A.D.Hodgkinson # |
511 | # # |
512 | # History: 20-Oct-2006 (ADH): Integrated into HubSsoLib. # |
513 | ####################################################################### |
514 | |
515 | module Core |
516 | |
517 | # Returns true or false if the user is logged in. |
518 | # |
519 | # Preloads @hubssolib_current_user with user data if logged in. |
520 | # |
521 | def hubssolib_logged_in? |
522 | user = self.hubssolib_current_user |
523 | return user && user != :false ? true : false |
524 | end |
525 | |
526 | # Check if the user is authorized to perform the current action. If calling |
527 | # from a helper, pass the action name and class name; otherwise by default, |
528 | # the current action name and 'self.class' will be used. |
529 | # |
530 | # Override this method in your controllers if you want to restrict access |
531 | # to a different set of actions. Presently, the current user's roles are |
532 | # compared against the caller's permissions hash and the action name. |
533 | # |
534 | def hubssolib_authorized?(action = action_name, classname = self.class) |
535 | |
536 | # Classes with no permissions object always authorise everything. |
537 | # Otherwise, ask the permissions object for its opinion. |
538 | |
539 | if (classname.respond_to? :hubssolib_permissions) |
540 | return classname.hubssolib_permissions.permitted?(self.hubssolib_current_user.roles, action) |
541 | else |
542 | return true |
543 | end |
544 | end |
545 | |
546 | # Is the current user privileged? Anything other than normal user |
547 | # privileges will suffice. Can be called if not logged in. Returns |
548 | # 'false' for logged out or normal user privileges only, else 'true'. |
549 | # |
550 | def hubssolib_privileged? |
551 | return false unless hubssolib_logged_in? |
552 | |
553 | pnormal = HubSsoLib::Roles.new(false).to_s |
554 | puser = self.hubssolib_current_user.roles.to_authenticated_roles.to_s |
555 | |
556 | return (puser && !puser.empty? && puser != pnormal) |
557 | end |
558 | |
559 | # Log out the user. Very few applications should ever need to call this, |
560 | # though Hub certainly does and it gets used internally too. |
561 | # |
562 | def hubssolib_log_out |
563 | # Causes the "hubssolib_current_user=" method to run, which |
564 | # deals with everything else. |
565 | self.hubssolib_current_user = nil |
566 | end |
567 | |
568 | # Accesses the current user from the cookie or internal cache. |
569 | # The cache is used because of the IMHO horrible Rails cookie |
570 | # API - a value we set right now, then read back, does not get |
571 | # returned. The values we read are from the last browser |
572 | # response, always; the values we set are to be sent out upon |
573 | # the next browser request, always. This means we can't just |
574 | # set values in the cookie and rely on being able to read them |
575 | # later, but before another request has been processed. Hence |
576 | # the cache. |
577 | # |
578 | def hubssolib_current_user |
579 | @hubssolib_current_user ||= hubssolib_get_user_data |
580 | end |
581 | |
582 | # Store the given user data in the cookie |
583 | # |
584 | def hubssolib_current_user=(new_user) |
585 | # We have to distinguish between "code just started running, |
586 | # @hubssolib_current_user not defined yet" and "someone set |
587 | # the user to 'nil' to log out" states. We do this by setting |
588 | # a user value of :false, a symbol, so that "user ||= thing" |
589 | # will continue to return ":false" rather than "thing". |
590 | |
591 | @hubssolib_current_user = new_user || :false |
592 | hubssolib_set_user_data(new_user) |
593 | end |
594 | |
595 | # Accesses the current session from the cookie. Creates a new |
596 | # session object if need be. |
597 | # |
598 | def hubssolib_current_session |
599 | @hubssolib_current_session ||= hubssolib_get_session_data || HubSsoLib::SessionData.new |
600 | end |
601 | |
602 | # Store the given session data in the cookie |
603 | # |
604 | def hubssolib_current_session=(new_session) |
605 | @hubssolib_current_session = new_session |
606 | hubssolib_set_session_data(new_session) |
607 | end |
608 | |
609 | # Return a human-readable unique ID for a user. We don't want to |
610 | # have e-mail addresses all over the place, but don't want to rely |
611 | # on real names as unique - they aren't. Instead, produce a |
612 | # composite of the user's account database ID (which must be |
613 | # unique by definition) and their real name. |
614 | # |
615 | def hubssolib_unique_name |
616 | user = hubssolib_current_user |
617 | user ? "#{user.real_name} (#{user.id})" : 'Anonymous' |
618 | end |
619 | |
620 | # Main filter method to implement HubSsoLib permissions management, |
621 | # session expiry and so-on. Called from controllers only. |
622 | # |
623 | def hubssolib_update_state |
624 | |
625 | # Does this action require a logged in user? |
626 | |
627 | if (self.class.respond_to? :hubssolib_permissions) |
628 | login_is_required = !self.class.hubssolib_permissions.permitted?('', action_name) |
629 | else |
630 | login_is_required = false |
631 | end |
632 | |
633 | # If we require login but we're logged out, redirect to Hub login. |
634 | |
635 | logged_in = hubssolib_logged_in? |
636 | |
637 | if (login_is_required and logged_in == false) |
638 | hubssolib_store_location |
639 | return hubssolib_must_login |
640 | end |
641 | |
642 | # If we reach here the user is either logged, or the method does |
643 | # not require them to be. In the latter case, if we're not logged |
644 | # in there is no more work to do - exit early. |
645 | |
646 | return true unless logged_in # true -> let action processing continue |
647 | |
648 | # So we reach here knowing we're logged in, but the action may or |
649 | # may not require authorisation. |
650 | |
651 | unless (login_is_required) |
652 | |
653 | # We have to update session expiry even for actions that don't |
654 | # need us to be logged in, since we *are* logged in and need to |
655 | # maintain that state. If, though, the session expires, we just |
656 | # quietly log out and let action processing carry on. |
657 | |
658 | if (hubssolib_session_expired?) |
659 | hubssolib_log_out |
660 | hubssolib_set_flash(:attention, 'Your session timed out, so you are no longer logged in.') |
661 | else |
662 | hubssolib_set_last_used(Time.now.utc) |
663 | end |
664 | |
665 | return true # true -> let action processing continue |
666 | |
667 | else |
668 | |
669 | # Login *is* required for this action. If the session expires, |
670 | # redirect to Hub's login page via its expiry action. Otherwise |
671 | # check authorisation and allow action processing to continue |
672 | # if OK, else indicate that access is denied. |
673 | |
674 | if (hubssolib_session_expired?) |
675 | hubssolib_store_location |
676 | hubssolib_log_out |
677 | hubssolib_set_flash(:attention, 'Sorry, your session timed out; you need to log in again to continue.') |
678 | |
679 | # We mean this: redirect_to :controller => 'account', :action => 'login' |
680 | # ...except for the Hub, rather than the current application (whatever |
681 | # it may be). |
682 | redirect_to HUB_PATH_PREFIX + '/account/login' |
683 | else |
684 | hubssolib_set_last_used(Time.now.utc) |
685 | return hubssolib_authorized? ? true : hubssolib_access_denied |
686 | end |
687 | |
688 | end |
689 | end |
690 | |
691 | # Store the URI of the current request in the session. |
692 | # |
693 | # We can return to this location by calling #redirect_back_or_default. |
694 | def hubssolib_store_location |
695 | hubssolib_set_return_to(request.request_uri) |
696 | end |
697 | |
698 | # Redirect to the URI stored by the most recent store_location call or |
699 | # to the passed default. |
700 | def hubssolib_redirect_back_or_default(default) |
701 | url = hubssolib_get_return_to |
702 | |
703 | url ? redirect_to_url(url) : redirect_to(default) |
704 | hubssolib_set_return_to(nil) |
705 | end |
706 | |
707 | # Ensure the current request is carried out over HTTPS by redirecting |
708 | # back to the current URL with the HTTPS protocol if it isn't. |
709 | # |
710 | def hubssolib_ensure_https |
711 | redirect_to({ :protocol => 'https://' }) unless request.ssl? |
712 | end |
713 | |
714 | # Public methods to set some data that would normally go in @session, |
715 | # but can't because it needs to be accessed across applications. It is |
716 | # put in an insecure support cookie instead. There are some related |
717 | # private methods for things like session expiry too. |
718 | # |
719 | def hubssolib_get_flash() |
720 | hubssolib_get_field(:flash) || {} |
721 | end |
722 | |
723 | def hubssolib_set_flash(symbol, message) |
724 | f = hubssolib_get_flash |
725 | f[symbol] = message |
726 | hubssolib_set_field(:flash=, f) |
727 | end |
728 | |
729 | def hubssolib_clear_flash |
730 | hubssolib_set_field(:flash=, {}) |
731 | end |
732 | |
733 | # Helper methods to output flash data. It isn't merged into the standard |
734 | # application flash with a filter because the rather daft and difficult |
735 | # to manage lifecycle model of the standard flash gets in the way. |
736 | # |
737 | # First, return tags for a flash using the given key, clearing the |
738 | # result in the flash hash now it has been used. |
739 | # |
740 | def hubssolib_flash_tag(key) |
741 | value = hubssolib_get_flash()[key] |
742 | |
743 | if (value) |
744 | hubssolib_set_flash(key, nil) |
745 | return "<h2 align=\"left\" class=\"#{key}\">#{value}</h2><p />" |
746 | else |
747 | return '' |
748 | end |
749 | end |
750 | |
751 | # Return tags for a standard application flash using the given key. |
752 | # |
753 | def hubssolib_standard_flash_tag(key) |
754 | value = flash[key] if (flash) |
755 | |
756 | if (value) |
757 | flash.delete(key) |
758 | return "<h2 align=\"left\" class=\"#{key}\">#{value}</h2><p />" |
759 | else |
760 | return '' |
761 | end |
762 | end |
763 | |
764 | # Return flash tags for known keys, then all remaining keys, from both |
765 | # the cross-application and standard standard flash hashes. |
766 | # |
767 | def hubssolib_flash_tags |
768 | # These known key values are used to guarantee an order in the output |
769 | # for cases where multiple messages are defined. |
770 | |
771 | tags = hubssolib_flash_tag(:notice) << |
772 | hubssolib_flash_tag(:attention) << |
773 | hubssolib_flash_tag(:alert) |
774 | |
775 | tags << hubssolib_standard_flash_tag(:notice) << |
776 | hubssolib_standard_flash_tag(:attention) << |
777 | hubssolib_standard_flash_tag(:alert) |
778 | |
779 | # Now pick up anything else. |
780 | |
781 | hubssolib_get_flash.each do |key, value| |
782 | tags << hubssolib_flash_tag(key) if (value) |
783 | end |
784 | |
785 | flash.each do |key, value| |
786 | tags << hubssolib_standard_flash_tag(key) if (value) |
787 | end |
788 | |
789 | return tags |
790 | end |
791 | |
792 | # Inclusion hook to make various methods available as ActionView |
793 | # helper methods. |
794 | def self.included(base) |
795 | base.send :helper_method, |
796 | :hubssolib_current_user, |
797 | :hubssolib_logged_in?, |
798 | :hubssolib_authorized?, |
799 | :hubssolib_privileged?, |
800 | :hubssolib_flash_tags |
801 | end |
802 | |
803 | private |
804 | |
805 | # Indicate that the user must log in to complete their request. |
806 | # Returns false to enable a before_filter to return through this |
807 | # method while ensuring that the previous action processing is |
808 | # halted (since the overall return value is therefore 'false'). |
809 | # |
810 | def hubssolib_must_login |
811 | hubssolib_set_flash(:alert, 'You must log in before you can continue.') |
812 | redirect_to HUB_PATH_PREFIX + '/account/login' |
813 | return false |
814 | end |
815 | |
816 | # Indicate access is denied for a given logged in user's request. |
817 | # Returns false to enable a before_filter to return through this |
818 | # method while ensuring that the previous action processing is |
819 | # halted (since the overall return value is therefore 'false'). |
820 | # |
821 | def hubssolib_access_denied |
822 | hubssolib_set_flash(:alert, 'You do not have permission to carry out that action on this site.') |
823 | redirect_to HUB_PATH_PREFIX + '/' |
824 | return false |
825 | end |
826 | |
827 | # Check conditions for session expiry. Returns 'true' if session's |
828 | # last_used date indicates expiry, else 'false'. |
829 | # |
830 | def hubssolib_session_expired? |
831 | |
832 | # 23-Oct-2006 (ADH): |
833 | # |
834 | # An exception, which is also a security hole of sorts. POST requests |
835 | # cannot be redirected because HTTP doesn't have that concept. If a user |
836 | # is editing a Wiki page, say, then goes away, comes back later and now |
837 | # finishes their edits, their session may have timed out. They submit |
838 | # the page but it's by POST so their submission details are lost. If they |
839 | # are lucky their browser might remember the form contents if they go |
840 | # back but not all do and not all users would think of doing that. |
841 | # |
842 | # To work around this, don't enforce a timeout for POST requests. Should |
843 | # a user on a public computer not log out, then a hacker arrive *after* |
844 | # the session expiry time (if they arrive before it expires then the |
845 | # except for POSTs is irrelevant), they could recover the session by |
846 | # constructing a POST request. It's a convoluted path, requires a user to |
847 | # have not logged out anyway, and the Hub isn't intended for Fort Knox. |
848 | # At the time of writing the trade-off of usability vs security is |
849 | # considered acceptable, though who knows, the view may change in future. |
850 | |
851 | last_used = hubssolib_get_last_used |
852 | (request.method != :post && last_used && Time.now.utc - last_used > HUBSSOLIB_IDLE_TIME_LIMIT) |
853 | end |
854 | |
855 | # Retrieve data from a given cookie with encrypted contents. |
856 | # |
857 | def hubssolib_get_cookie_data(name) |
858 | crypto = HubSsoLib::Crypto.new(HUBSSOLIB_RND_FILE_PATH) |
859 | passphrase = crypto.scramble_passphrase(request.remote_ip) |
860 | data = cookies[name] |
861 | user = nil |
862 | |
863 | if (data && !data.empty?) |
864 | user = Marshal.load(crypto.decode(data, passphrase)) |
865 | end |
866 | |
867 | return user |
868 | rescue |
869 | return nil |
870 | end |
871 | |
872 | # Set the given cookie to a value of the given data, which |
873 | # will be encrypted. |
874 | # |
875 | def hubssolib_set_cookie_data(name, value) |
876 | if (value.nil?) |
877 | # Using cookies.delete *should* work but doesn't. Set the |
878 | # cookie with nil data instead. |
879 | data = nil |
880 | else |
881 | crypto = HubSsoLib::Crypto.new(HUBSSOLIB_RND_FILE_PATH) |
882 | passphrase = crypto.scramble_passphrase(request.remote_ip) |
883 | data = crypto.encode(Marshal.dump(value), passphrase) |
884 | end |
885 | |
886 | # No expiry time; to aid security, use session cookies only. |
887 | |
888 | cookies[name] = { |
889 | :value => data, |
890 | :path => '/rails', |
891 | :secure => true |
892 | } |
893 | end |
894 | |
895 | # Retrieve user data from the session data cookie. |
896 | # |
897 | def hubssolib_get_user_data |
898 | return hubssolib_get_cookie_data(HUBSSOLIB_SESSION_DATA_KEY) |
899 | end |
900 | |
901 | # Store user data in the session data cookie. Pass the user to store, |
902 | # or 'nil' to clear the cookie. |
903 | # |
904 | def hubssolib_set_user_data(user) |
905 | hubssolib_set_cookie_data(HUBSSOLIB_SESSION_DATA_KEY, user) |
906 | end |
907 | |
908 | # Retrieve session data from the session support cookie. |
909 | # |
910 | def hubssolib_get_session_data |
911 | return hubssolib_get_cookie_data(HUBSSOLIB_SESSION_SUPPORT_KEY) |
912 | end |
913 | |
914 | # Store session data in the session support cookie. Pass the session |
915 | # tostore, or 'nil' to clear the cookie. |
916 | # |
917 | def hubssolib_set_session_data(support) |
918 | hubssolib_set_cookie_data(HUBSSOLIB_SESSION_SUPPORT_KEY, support) |
919 | end |
920 | |
921 | # Retrieve data from the session support cookie, with lazy initialisation. |
922 | # |
923 | def hubssolib_get_field(field) |
924 | data = self.hubssolib_current_session |
925 | return data.send(field) |
926 | rescue |
927 | return nil |
928 | end |
929 | |
930 | # Set data in the session support cookie, merging with existing values and |
931 | # supporting lazy initialisation. Returns 'true' if successful, else 'false'. |
932 | # |
933 | def hubssolib_set_field(field, value) |
934 | data = self.hubssolib_current_session |
935 | data.send(field, value) |
936 | self.hubssolib_current_session = data |
937 | |
938 | return true |
939 | rescue |
940 | return false |
941 | end |
942 | |
943 | # Methods for session support using an independent, insecure cookie. |
944 | |
945 | def hubssolib_get_last_used |
946 | hubssolib_get_field(:last_used) |
947 | end |
948 | |
949 | def hubssolib_set_last_used(time) |
950 | hubssolib_set_field(:last_used=, time) |
951 | end |
952 | |
953 | def hubssolib_get_return_to |
954 | hubssolib_get_field(:return_to) |
955 | end |
956 | |
957 | def hubssolib_set_return_to(url) |
958 | hubssolib_set_field(:return_to=, url) |
959 | end |
960 | |
961 | end # Core module |
962 | end # HubSsoLib module |
963 | |
964 | ####################################################################### |
965 | # Classes: Standard class extensions for HubSsoLib Roles operations. # |
966 | # By Hipposoft, 2006 # |
967 | # # |
968 | # Purpose: Extensions to standard classes to support HubSsoLib. # |
969 | # # |
970 | # Author: A.D.Hodgkinson # |
971 | # # |
972 | # History: 20-Oct-2006 (ADH): Integrated into HubSsoLib. # |
973 | ####################################################################### |
974 | |
975 | # Method to return a Roles object created from the |
976 | # contents of the String the method is invoked upon. The |
977 | # string may contain a single role or a comma-separated list |
978 | # with no white space. |
979 | # |
980 | class String |
981 | def to_authenticated_roles |
982 | roles = HubSsoLib::Roles.new |
983 | array = self.split(',') |
984 | |
985 | roles.clear |
986 | array.each { |role| roles.add(role) } |
987 | |
988 | return roles |
989 | end |
990 | end # String class |
991 | |
992 | # Method to return a Roles object created from the |
993 | # contents of the Symbol the method is invoked upon. |
994 | # |
995 | class Symbol |
996 | def to_authenticated_roles |
997 | return self.to_s.to_authenticated_roles |
998 | end |
999 | end # Symbol class |
1000 | |
1001 | # Method to return a Roles object created from the |
1002 | # contents of the Array the method is invoked upon. The array |
1003 | # contents will be flattened. After that, each entry must be |
1004 | # a single role symbol or string equivalent. Comma-separated |
1005 | # lists are not currently allowed (improvements to the roles |
1006 | # class could easily give this, but the bloat isn't needed). |
1007 | # |
1008 | class Array |
1009 | def to_authenticated_roles |
1010 | roles = HubSsoLib::Roles.new |
1011 | roles.clear |
1012 | |
1013 | self.flatten.each { |entry| roles.add(entry.to_s) } |
1014 | |
1015 | return roles |
1016 | end |
1017 | end # Array class |