authlogic 1.4.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of authlogic might be problematic. Click here for more details.
- data/CHANGELOG.rdoc +19 -0
- data/Manifest.txt +111 -0
- data/README.rdoc +116 -389
- data/Rakefile +14 -7
- data/lib/authlogic.rb +33 -35
- data/lib/authlogic/acts_as_authentic/base.rb +91 -0
- data/lib/authlogic/acts_as_authentic/email.rb +77 -0
- data/lib/authlogic/acts_as_authentic/logged_in_status.rb +54 -0
- data/lib/authlogic/acts_as_authentic/login.rb +65 -0
- data/lib/authlogic/acts_as_authentic/magic_columns.rb +24 -0
- data/lib/authlogic/acts_as_authentic/password.rb +215 -0
- data/lib/authlogic/acts_as_authentic/perishable_token.rb +100 -0
- data/lib/authlogic/acts_as_authentic/persistence_token.rb +66 -0
- data/lib/authlogic/acts_as_authentic/restful_authentication.rb +60 -0
- data/lib/authlogic/acts_as_authentic/session_maintenance.rb +127 -0
- data/lib/authlogic/acts_as_authentic/single_access_token.rb +58 -0
- data/lib/authlogic/acts_as_authentic/validations_scope.rb +32 -0
- data/lib/authlogic/{session/authenticates_many_association.rb → authenticates_many/association.rb} +10 -6
- data/lib/authlogic/authenticates_many/base.rb +55 -0
- data/lib/authlogic/controller_adapters/abstract_adapter.rb +2 -3
- data/lib/authlogic/controller_adapters/merb_adapter.rb +0 -4
- data/lib/authlogic/controller_adapters/rails_adapter.rb +0 -4
- data/lib/authlogic/crypto_providers/aes256.rb +0 -2
- data/lib/authlogic/crypto_providers/bcrypt.rb +0 -2
- data/lib/authlogic/crypto_providers/md5.rb +34 -0
- data/lib/authlogic/crypto_providers/sha1.rb +0 -2
- data/lib/authlogic/crypto_providers/sha512.rb +1 -3
- data/lib/authlogic/i18n.rb +1 -4
- data/lib/authlogic/random.rb +33 -0
- data/lib/authlogic/session/activation.rb +56 -0
- data/lib/authlogic/session/active_record_trickery.rb +15 -7
- data/lib/authlogic/session/base.rb +31 -456
- data/lib/authlogic/session/brute_force_protection.rb +50 -27
- data/lib/authlogic/session/callbacks.rb +24 -15
- data/lib/authlogic/session/cookies.rb +108 -22
- data/lib/authlogic/session/existence.rb +89 -0
- data/lib/authlogic/session/foundation.rb +63 -0
- data/lib/authlogic/session/http_auth.rb +23 -0
- data/lib/authlogic/session/id.rb +41 -0
- data/lib/authlogic/session/klass.rb +75 -0
- data/lib/authlogic/session/magic_columns.rb +75 -0
- data/lib/authlogic/session/magic_states.rb +58 -0
- data/lib/authlogic/session/params.rb +82 -19
- data/lib/authlogic/session/password.rb +156 -0
- data/lib/authlogic/session/{perishability.rb → perishable_token.rb} +4 -4
- data/lib/authlogic/session/persistence.rb +70 -0
- data/lib/authlogic/session/priority_record.rb +34 -0
- data/lib/authlogic/session/scopes.rb +57 -53
- data/lib/authlogic/session/session.rb +46 -31
- data/lib/authlogic/session/timeout.rb +65 -31
- data/lib/authlogic/session/unauthorized_record.rb +50 -0
- data/lib/authlogic/session/validation.rb +76 -0
- data/lib/authlogic/testing/test_unit_helpers.rb +3 -3
- data/lib/authlogic/version.rb +3 -3
- data/test/acts_as_authentic_test/base_test.rb +12 -0
- data/test/acts_as_authentic_test/email_test.rb +79 -0
- data/test/acts_as_authentic_test/logged_in_status_test.rb +36 -0
- data/test/acts_as_authentic_test/login_test.rb +79 -0
- data/test/acts_as_authentic_test/magic_columns_test.rb +27 -0
- data/test/acts_as_authentic_test/password_test.rb +212 -0
- data/test/acts_as_authentic_test/perishable_token_test.rb +56 -0
- data/test/acts_as_authentic_test/persistence_token_test.rb +55 -0
- data/test/acts_as_authentic_test/session_maintenance_test.rb +68 -0
- data/test/acts_as_authentic_test/single_access_test.rb +39 -0
- data/test/authenticates_many_test.rb +16 -0
- data/test/{crypto_provider_tests → crypto_provider_test}/aes256_test.rb +1 -1
- data/test/{crypto_provider_tests → crypto_provider_test}/bcrypt_test.rb +1 -1
- data/test/{crypto_provider_tests → crypto_provider_test}/sha1_test.rb +1 -1
- data/test/{crypto_provider_tests → crypto_provider_test}/sha512_test.rb +1 -1
- data/test/fixtures/employees.yml +4 -4
- data/test/fixtures/users.yml +6 -6
- data/test/libs/company.rb +6 -0
- data/test/libs/employee.rb +7 -0
- data/test/libs/employee_session.rb +2 -0
- data/test/libs/project.rb +3 -0
- data/test/libs/user_session.rb +2 -0
- data/test/random_test.rb +49 -0
- data/test/session_test/activation_test.rb +43 -0
- data/test/session_test/active_record_trickery_test.rb +26 -0
- data/test/session_test/brute_force_protection_test.rb +76 -0
- data/test/session_test/callbacks_test.rb +6 -0
- data/test/session_test/cookies_test.rb +107 -0
- data/test/session_test/credentials_test.rb +0 -0
- data/test/session_test/existence_test.rb +64 -0
- data/test/session_test/http_auth_test.rb +16 -0
- data/test/session_test/id_test.rb +17 -0
- data/test/session_test/klass_test.rb +35 -0
- data/test/session_test/magic_columns_test.rb +59 -0
- data/test/session_test/magic_states_test.rb +60 -0
- data/test/session_test/params_test.rb +53 -0
- data/test/session_test/password_test.rb +84 -0
- data/test/{session_tests → session_test}/perishability_test.rb +1 -1
- data/test/session_test/persistence_test.rb +21 -0
- data/test/{session_tests → session_test}/scopes_test.rb +2 -3
- data/test/session_test/session_test.rb +59 -0
- data/test/session_test/timeout_test.rb +43 -0
- data/test/session_test/unauthorized_record_test.rb +13 -0
- data/test/session_test/validation_test.rb +23 -0
- data/test/test_helper.rb +14 -29
- metadata +120 -112
- data/Manifest +0 -76
- data/authlogic.gemspec +0 -38
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/base.rb +0 -22
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/config.rb +0 -238
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/credentials.rb +0 -155
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/logged_in.rb +0 -51
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/perishability.rb +0 -71
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/persistence.rb +0 -94
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/session_maintenance.rb +0 -87
- data/lib/authlogic/orm_adapters/active_record_adapter/acts_as_authentic/single_access.rb +0 -61
- data/lib/authlogic/orm_adapters/active_record_adapter/authenticates_many.rb +0 -58
- data/lib/authlogic/session/config.rb +0 -421
- data/lib/authlogic/session/errors.rb +0 -18
- data/lib/authlogic/session/record_info.rb +0 -24
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/config_test.rb +0 -154
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/credentials_test.rb +0 -157
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/logged_in_test.rb +0 -24
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/perishability_test.rb +0 -41
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/persistence_test.rb +0 -54
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/session_maintenance_test.rb +0 -62
- data/test/orm_adapters_tests/active_record_adapter_tests/acts_as_authentic_tests/single_access_test.rb +0 -41
- data/test/orm_adapters_tests/active_record_adapter_tests/authenticates_many_test.rb +0 -32
- data/test/session_tests/active_record_trickery_test.rb +0 -14
- data/test/session_tests/authenticates_many_association_test.rb +0 -28
- data/test/session_tests/base_test.rb +0 -307
- data/test/session_tests/brute_force_protection_test.rb +0 -53
- data/test/session_tests/config_test.rb +0 -184
- data/test/session_tests/cookies_test.rb +0 -32
- data/test/session_tests/params_test.rb +0 -32
- data/test/session_tests/session_test.rb +0 -45
- data/test/session_tests/timeout_test.rb +0 -71
data/lib/authlogic/{session/authenticates_many_association.rb → authenticates_many/association.rb}
RENAMED
@@ -1,15 +1,19 @@
|
|
1
1
|
module Authlogic
|
2
|
-
module
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# An object of this class is used as a proxy for the authenticates_many relationship. It basically allows you to "save" scope details and call them on an object, which allows you to do the following:
|
2
|
+
module AuthenticatesMany
|
3
|
+
# An object of this class is used as a proxy for the authenticates_many relationship. It basically allows you to "save" scope details
|
4
|
+
# and call them on an object, which allows you to do the following:
|
6
5
|
#
|
7
6
|
# @account.user_sessions.new
|
8
7
|
# @account.user_sessions.find
|
9
8
|
# # ... etc
|
10
9
|
#
|
11
|
-
# You can call all of the class level methods off of an object with a saved scope, so that calling the above methods scopes the user
|
12
|
-
|
10
|
+
# You can call all of the class level methods off of an object with a saved scope, so that calling the above methods scopes the user
|
11
|
+
# sessions down to that specific account. To implement this via ActiveRecord do something like:
|
12
|
+
#
|
13
|
+
# class User < ActiveRecord::Base
|
14
|
+
# authenticates_many :user_sessions
|
15
|
+
# end
|
16
|
+
class Association
|
13
17
|
attr_accessor :klass, :find_options, :id
|
14
18
|
|
15
19
|
def initialize(klass, find_options, id)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Authlogic
|
2
|
+
# This allows you to scope your authentication. For example, let's say all users belong to an account, you want to make sure only users
|
3
|
+
# that belong to that account can actually login into that account. Simple, just do:
|
4
|
+
#
|
5
|
+
# class Account < ActiveRecord::Base
|
6
|
+
# authenticates_many :user_sessions
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# Now you can scope sessions just like everything else in ActiveRecord:
|
10
|
+
#
|
11
|
+
# @account.user_sessions.new(*args)
|
12
|
+
# @account.user_sessions.create(*args)
|
13
|
+
# @account.user_sessions.find(*args)
|
14
|
+
# # ... etc
|
15
|
+
#
|
16
|
+
# Checkout the authenticates_many method for a list of options.
|
17
|
+
# You may also want to checkout Authlogic::ActsAsAuthentic::Scope to scope your model.
|
18
|
+
module AuthenticatesMany
|
19
|
+
module Base
|
20
|
+
# Allows you set essentially set up a relationship with your sessions. See module definition above for more details.
|
21
|
+
#
|
22
|
+
# === Options
|
23
|
+
#
|
24
|
+
# * <tt>session_class:</tt> default: "#{name}Session",
|
25
|
+
# This is the related session class.
|
26
|
+
#
|
27
|
+
# * <tt>relationship_name:</tt> default: options[:session_class].klass_name.underscore.pluralize,
|
28
|
+
# This is the name of the relationship you want to use to scope everything. For example an Account has many Users. There should be a relationship
|
29
|
+
# called :users that you defined with a has_many. The reason we use the relationship is so you don't have to repeat yourself. The relatonship
|
30
|
+
# could have all kinds of custom options. So instead of repeating yourself we essentially use the scope that the relationship creates.
|
31
|
+
#
|
32
|
+
# * <tt>find_options:</tt> default: nil,
|
33
|
+
# By default the find options are created from the relationship you specify with :relationship_name. But if you want to override this and
|
34
|
+
# manually specify find_options you can do it here. Specify options just as you would in ActiveRecord::Base.find.
|
35
|
+
#
|
36
|
+
# * <tt>scope_cookies:</tt> default: false
|
37
|
+
# By the nature of cookies they scope theirself if you are using subdomains to access accounts. If you aren't using subdomains you need to have
|
38
|
+
# separate cookies for each account, assuming a user is logging into mroe than one account. Authlogic can take care of this for you by
|
39
|
+
# prefixing the name of the cookie and sessin with the model id. You just need to tell Authlogic to do this by passing this option.
|
40
|
+
def authenticates_many(name, options = {})
|
41
|
+
options[:session_class] ||= name.to_s.classify.constantize
|
42
|
+
options[:relationship_name] ||= options[:session_class].klass_name.underscore.pluralize
|
43
|
+
class_eval <<-"end_eval", __FILE__, __LINE__
|
44
|
+
def #{name}
|
45
|
+
find_options = #{options[:find_options].inspect} || #{options[:relationship_name]}.scope(:find)
|
46
|
+
find_options.delete_if { |key, value| ![:conditions, :include, :joins].include?(key.to_sym) || value.nil? }
|
47
|
+
@#{name} ||= Authlogic::AuthenticatesMany::Association.new(#{options[:session_class]}, find_options, #{options[:scope_cookies] ? "self.class.model_name.underscore + '_' + self.send(self.class.primary_key).to_s" : "nil"})
|
48
|
+
end
|
49
|
+
end_eval
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
::ActiveRecord::Base.extend(Base) if defined?(::ActiveRecord)
|
54
|
+
end
|
55
|
+
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
module Authlogic
|
2
2
|
module ControllerAdapters # :nodoc:
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# Allows you to use Authlogic in any framework you want, not just rails. See the RailsAdapter or MerbAdapter for an example of how to adapt Authlogic to work with your framework.
|
3
|
+
# Allows you to use Authlogic in any framework you want, not just rails. See the RailsAdapter or MerbAdapter
|
4
|
+
# for an example of how to adapt Authlogic to work with your framework.
|
6
5
|
class AbstractAdapter
|
7
6
|
attr_accessor :controller
|
8
7
|
|
@@ -1,12 +1,8 @@
|
|
1
1
|
module Authlogic
|
2
2
|
module ControllerAdapters
|
3
|
-
# = Merb Adapter
|
4
|
-
#
|
5
3
|
# Adapts authlogic to work with merb. The point is to close the gap between what authlogic expects and what the merb controller object
|
6
4
|
# provides. Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, etc.
|
7
5
|
class MerbAdapter < AbstractAdapter
|
8
|
-
# = Merb Implementation
|
9
|
-
#
|
10
6
|
# Lets Authlogic know about the controller object via a before filter, AKA "activates" authlogic.
|
11
7
|
module MerbImplementation
|
12
8
|
def self.included(klass) # :nodoc:
|
@@ -1,7 +1,5 @@
|
|
1
1
|
module Authlogic
|
2
2
|
module ControllerAdapters
|
3
|
-
# = Rails Adapter
|
4
|
-
#
|
5
3
|
# Adapts authlogic to work with rails. The point is to close the gap between what authlogic expects and what the rails controller object
|
6
4
|
# provides. Similar to how ActiveRecord has an adapter for MySQL, PostgreSQL, SQLite, etc.
|
7
5
|
class RailsAdapter < AbstractAdapter
|
@@ -22,8 +20,6 @@ module Authlogic
|
|
22
20
|
request.format.to_s
|
23
21
|
end
|
24
22
|
|
25
|
-
# = Rails Implementation
|
26
|
-
#
|
27
23
|
# Lets Authlogic know about the controller object via a before filter, AKA "activates" authlogic.
|
28
24
|
module RailsImplementation
|
29
25
|
def self.included(klass) # :nodoc:
|
@@ -2,8 +2,6 @@ require "openssl"
|
|
2
2
|
|
3
3
|
module Authlogic
|
4
4
|
module CryptoProviders
|
5
|
-
# = AES256
|
6
|
-
#
|
7
5
|
# This encryption method is reversible if you have the supplied key. So in order to use this encryption method you must supply it with a key first.
|
8
6
|
# In an initializer, or before your application initializes, you should do the following:
|
9
7
|
#
|
@@ -5,8 +5,6 @@ end
|
|
5
5
|
|
6
6
|
module Authlogic
|
7
7
|
module CryptoProviders
|
8
|
-
# = Bcrypt
|
9
|
-
#
|
10
8
|
# For most apps Sha512 is plenty secure, but if you are building an app that stores nuclear launch codes you might want to consier BCrypt. This is an extremely
|
11
9
|
# secure hashing algorithm, mainly because it is slow. A brute force attack on a BCrypt encrypted password would take much longer than a brute force attack on a
|
12
10
|
# password encrypted with a Sha algorithm. Keep in mind you are sacrificing performance by using this, generating a password takes exponentially longer than any
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require "digest/md5"
|
2
|
+
|
3
|
+
module Authlogic
|
4
|
+
module CryptoProviders
|
5
|
+
# This class was made for the users transitioning from md5 based systems.
|
6
|
+
# I highly discourage using this crypto provider as it superbly inferior
|
7
|
+
# to your other options.
|
8
|
+
#
|
9
|
+
# Please use any other provider offered by Authlogic.
|
10
|
+
class MD5
|
11
|
+
class << self
|
12
|
+
attr_accessor :join_token
|
13
|
+
|
14
|
+
# The number of times to loop through the encryption.
|
15
|
+
def stretches
|
16
|
+
@stretches ||= 1
|
17
|
+
end
|
18
|
+
attr_writer :stretches
|
19
|
+
|
20
|
+
# Turns your raw password into a MD5 hash.
|
21
|
+
def encrypt(*tokens)
|
22
|
+
digest = tokens.flatten.join(join_token)
|
23
|
+
stretches.times { digest = Digest::MD5.hexdigest(digest) }
|
24
|
+
digest
|
25
|
+
end
|
26
|
+
|
27
|
+
# Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
|
28
|
+
def matches?(crypted, *tokens)
|
29
|
+
encrypt(*tokens) == crypted
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -2,8 +2,6 @@ require "digest/sha1"
|
|
2
2
|
|
3
3
|
module Authlogic
|
4
4
|
module CryptoProviders
|
5
|
-
# = Sha1
|
6
|
-
#
|
7
5
|
# This class was made for the users transitioning from restful_authentication. I highly discourage using this crypto provider as it inferior to your other options.
|
8
6
|
# Please use any other provider offered by Authlogic.
|
9
7
|
class Sha1
|
@@ -1,9 +1,7 @@
|
|
1
1
|
require "digest/sha2"
|
2
2
|
|
3
3
|
module Authlogic
|
4
|
-
#
|
5
|
-
#
|
6
|
-
# The acts_as_authentic method allows you to pass a :crypto_provider option. This allows you to use any type of encryption you like.
|
4
|
+
# The acts_as_authentic method has a crypto_provider option. This allows you to use any type of encryption you like.
|
7
5
|
# Just create a class with a class level encrypt and matches? method. See example below.
|
8
6
|
#
|
9
7
|
# === Example
|
data/lib/authlogic/i18n.rb
CHANGED
@@ -1,6 +1,4 @@
|
|
1
1
|
module Authlogic
|
2
|
-
# I18n
|
3
|
-
#
|
4
2
|
# This class allows any message in Authlogic to use internationalization. In earlier versions of Authlogic each message was translated via configuration.
|
5
3
|
# This cluttered up the configuration and cluttered up Authlogic. So all translation has been extracted out into this class. Now all messages pass through
|
6
4
|
# this class, making it much easier to implement in I18n library / plugin you want. Use this as a layer that sits between Authlogic and whatever I18n
|
@@ -40,8 +38,7 @@ module Authlogic
|
|
40
38
|
# not_active: Your account is not active
|
41
39
|
# not_confirmed: Your account is not confirmed
|
42
40
|
# not_approved: Your account is not approved
|
43
|
-
#
|
44
|
-
# new_record: You can not login with a new record
|
41
|
+
# no_authentication_details: You did not provide any details for authentication.
|
45
42
|
class I18n
|
46
43
|
class << self
|
47
44
|
# All message translation is passed to this method. The first argument is the key for the message. The second is options, see the rails I18n library for a list of options used.
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Authlogic
|
2
|
+
# Handles generating random strings. If SecureRandom is installed it will default to this and use it instead. SecureRandom comes with ActiveSupport.
|
3
|
+
# So if you are using this in a rails app you should have this library.
|
4
|
+
module Random
|
5
|
+
extend self
|
6
|
+
|
7
|
+
SecureRandom = (defined?(::SecureRandom) && ::SecureRandom) || (defined?(::ActiveSupport::SecureRandom) && ::ActiveSupport::SecureRandom)
|
8
|
+
|
9
|
+
if SecureRandom
|
10
|
+
def hex_token
|
11
|
+
SecureRandom.hex(64)
|
12
|
+
end
|
13
|
+
|
14
|
+
def friendly_token
|
15
|
+
# use base64url as defined by RFC4648
|
16
|
+
SecureRandom.base64(15).tr('+/=', '-_ ').strip.delete("\n")
|
17
|
+
end
|
18
|
+
else
|
19
|
+
def hex_token
|
20
|
+
Authlogic::CryptoProviders::Sha512.encrypt(Time.now.to_s + (1..10).collect{ rand.to_s }.join)
|
21
|
+
end
|
22
|
+
|
23
|
+
FRIENDLY_CHARS = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
|
24
|
+
|
25
|
+
def friendly_token
|
26
|
+
newpass = ""
|
27
|
+
1.upto(20) { |i| newpass << FRIENDLY_CHARS[rand(FRIENDLY_CHARS.size-1)] }
|
28
|
+
newpass
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Authlogic
|
2
|
+
module Session
|
3
|
+
# Activating Authlogic requires that you pass it an Authlogic::ControllerAdapters::AbstractAdapter object, or a class that extends it.
|
4
|
+
# This is sort of like a database connection for an ORM library, Authlogic can't do anything until it is "connected" to a controller.
|
5
|
+
# If you are using a supported framework, Authlogic takes care of this for you.
|
6
|
+
module Activation
|
7
|
+
class NotActivatedError < ::StandardError # :nodoc:
|
8
|
+
def initialize(session)
|
9
|
+
super("You must activate the Authlogic::Session::Base.controller with a controller object before creating objects")
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.included(klass)
|
14
|
+
klass.class_eval do
|
15
|
+
extend ClassMethods
|
16
|
+
include InstanceMethods
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ClassMethods
|
21
|
+
# Returns true if a controller has been set and can be used properly. This MUST be set before anything can be done.
|
22
|
+
# Similar to how ActiveRecord won't allow you to do anything without establishing a DB connection. In your framework
|
23
|
+
# environment this is done for you, but if you are using Authlogic outside of your framework, you need to assign a controller
|
24
|
+
# object to Authlogic via Authlogic::Session::Base.controller = obj. See the controller= method for more information.
|
25
|
+
def activated?
|
26
|
+
!controller.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
# This accepts a controller object wrapped with the Authlogic controller adapter. The controller adapters close the gap
|
30
|
+
# between the different controllers in each framework. That being said, Authlogic is expecting your object's class to
|
31
|
+
# extend Authlogic::ControllerAdapters::AbstractAdapter. See Authlogic::ControllerAdapters for more info.
|
32
|
+
def controller=(value)
|
33
|
+
Thread.current[:authlogic_controller] = value
|
34
|
+
end
|
35
|
+
|
36
|
+
# The current controller object
|
37
|
+
def controller
|
38
|
+
Thread.current[:authlogic_controller]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
module InstanceMethods
|
43
|
+
# Making sure we are activated before we start creating objects
|
44
|
+
def initialize(*args)
|
45
|
+
raise NotActivatedError.new(self) unless self.class.activated?
|
46
|
+
super
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
def controller
|
51
|
+
self.class.controller
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -1,22 +1,30 @@
|
|
1
1
|
module Authlogic
|
2
2
|
module Session
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
#
|
3
|
+
# Authlogic looks like ActiveRecord, sounds like ActiveRecord, but its not ActiveRecord. That's the goal here.
|
4
|
+
# This is useful for the various rails helper methods such as form_for, error_messages_for, or any method that
|
5
|
+
# expects an ActiveRecord object. The point is to disguise the object as an ActiveRecord object so we can take
|
6
|
+
# advantage of the many ActiveRecord tools.
|
7
7
|
module ActiveRecordTrickery
|
8
|
-
def self.included(klass)
|
8
|
+
def self.included(klass)
|
9
9
|
klass.extend ClassMethods
|
10
10
|
klass.send(:include, InstanceMethods)
|
11
11
|
end
|
12
12
|
|
13
|
-
module ClassMethods
|
13
|
+
module ClassMethods
|
14
14
|
def human_attribute_name(*args)
|
15
15
|
klass.human_attribute_name(*args)
|
16
16
|
end
|
17
|
+
|
18
|
+
def human_name(*args)
|
19
|
+
klass.human_name(*args)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self_and_descendents_from_active_record
|
23
|
+
[self]
|
24
|
+
end
|
17
25
|
end
|
18
26
|
|
19
|
-
module InstanceMethods
|
27
|
+
module InstanceMethods
|
20
28
|
def new_record?
|
21
29
|
new_session?
|
22
30
|
end
|
@@ -1,462 +1,37 @@
|
|
1
1
|
module Authlogic
|
2
2
|
module Session # :nodoc:
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# This is the muscle behind Authlogic. For detailed information on how to use this please refer to the README. For detailed method explanations see below.
|
3
|
+
# This is the base class Authlogic, where all modules are included. For information on functiionality see the various
|
4
|
+
# sub modules.
|
6
5
|
class Base
|
7
|
-
include
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
# Same as create but calls create!, which raises an exception when authentication fails.
|
39
|
-
def create!(*args)
|
40
|
-
session = new(*args)
|
41
|
-
session.save!
|
42
|
-
end
|
43
|
-
|
44
|
-
# A convenience method for session.find_record. Finds your session by parameters, then session, then cookie, and finally by basic http auth.
|
45
|
-
# This is perfect for persisting your session:
|
46
|
-
#
|
47
|
-
# helper_method :current_user_session, :current_user
|
48
|
-
#
|
49
|
-
# def current_user_session
|
50
|
-
# return @current_user_session if defined?(@current_user_session)
|
51
|
-
# @current_user_session = UserSession.find
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# def current_user
|
55
|
-
# return @current_user if defined?(@current_user)
|
56
|
-
# @current_user = current_user_session && current_user_session.user
|
57
|
-
# end
|
58
|
-
#
|
59
|
-
# Accepts a single parameter as the id, to find session that you marked with an id:
|
60
|
-
#
|
61
|
-
# UserSession.find(:secure)
|
62
|
-
#
|
63
|
-
# See the id method for more information on ids.
|
64
|
-
def find(id = nil, priority_record = nil)
|
65
|
-
session = new(id)
|
66
|
-
session.before_find
|
67
|
-
if record = session.find_record
|
68
|
-
session.after_find
|
69
|
-
record.save_without_session_maintenance(false) if record.changed? && record != priority_record
|
70
|
-
session
|
71
|
-
else
|
72
|
-
nil
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
# The name of the class that this session is authenticating with. For example, the UserSession class will authenticate with the User class
|
77
|
-
# unless you specify otherwise in your configuration. See authenticate_with for information on how to change this value.
|
78
|
-
def klass
|
79
|
-
@klass ||=
|
80
|
-
if klass_name
|
81
|
-
klass_name.constantize
|
82
|
-
else
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# Same as klass, just returns a string instead of the actual constant.
|
88
|
-
def klass_name
|
89
|
-
@klass_name ||=
|
90
|
-
if guessed_name = name.scan(/(.*)Session/)[0]
|
91
|
-
@klass_name = guessed_name[0]
|
92
|
-
end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
attr_accessor :attempted_record, :new_session, :record
|
97
|
-
attr_reader :unauthorized_record
|
98
|
-
attr_writer :authenticating_with, :id, :persisting
|
99
|
-
|
100
|
-
# You can initialize a session by doing any of the following:
|
101
|
-
#
|
102
|
-
# UserSession.new
|
103
|
-
# UserSession.new(:login => "login", :password => "password", :remember_me => true)
|
104
|
-
# UserSession.new(User.first, true)
|
105
|
-
#
|
106
|
-
# If a user has more than one session you need to pass an id so that Authlogic knows how to differentiate the sessions. The id MUST be a Symbol.
|
107
|
-
#
|
108
|
-
# UserSession.new(:my_id)
|
109
|
-
# UserSession.new({:login => "login", :password => "password", :remember_me => true}, :my_id)
|
110
|
-
# UserSession.new(User.first, true, :my_id)
|
111
|
-
#
|
112
|
-
# For more information on ids see the id method.
|
113
|
-
#
|
114
|
-
# Lastly, the reason the id is separate from the first parameter hash is becuase this should be controlled by you, not by what the user passes.
|
115
|
-
# A user could inject their own id and things would not work as expected.
|
116
|
-
def initialize(*args)
|
117
|
-
raise NotActivated.new(self) unless self.class.activated?
|
118
|
-
|
119
|
-
create_configurable_methods!
|
120
|
-
|
121
|
-
self.id = args.pop if args.last.is_a?(Symbol)
|
122
|
-
|
123
|
-
if args.first.is_a?(Hash)
|
124
|
-
self.credentials = args.first
|
125
|
-
elsif !args.first.blank? && args.first.class < ::ActiveRecord::Base
|
126
|
-
self.unauthorized_record = args.first
|
127
|
-
self.remember_me = args[1] if args.size > 1
|
128
|
-
end
|
129
|
-
end
|
130
|
-
|
131
|
-
# A flag for how the user is logging in. Possible values:
|
132
|
-
#
|
133
|
-
# * <tt>:password</tt> - username and password
|
134
|
-
# * <tt>:unauthorized_record</tt> - an actual ActiveRecord object
|
135
|
-
#
|
136
|
-
# By default this is :password
|
137
|
-
def authenticating_with
|
138
|
-
@authenticating_with ||= :password
|
139
|
-
end
|
140
|
-
|
141
|
-
# Returns true if logging in with credentials. Credentials mean username and password.
|
142
|
-
def authenticating_with_password?
|
143
|
-
authenticating_with == :password
|
144
|
-
end
|
145
|
-
|
146
|
-
# Returns true if logging in with an unauthorized record
|
147
|
-
def authenticating_with_unauthorized_record?
|
148
|
-
authenticating_with == :unauthorized_record
|
149
|
-
end
|
150
|
-
alias_method :authenticating_with_record?, :authenticating_with_unauthorized_record?
|
151
|
-
|
152
|
-
# Your login credentials in hash format. Usually {:login => "my login", :password => "<protected>"} depending on your configuration.
|
153
|
-
# Password is protected as a security measure. The raw password should never be publicly accessible.
|
154
|
-
def credentials
|
155
|
-
{login_field => send(login_field), password_field => "<Protected>"}
|
156
|
-
end
|
157
|
-
|
158
|
-
# Lets you set your loging and password via a hash format. This is "params" safe. It only allows for 3 keys: your login field name, password field name, and remember me.
|
159
|
-
def credentials=(values)
|
160
|
-
return if values.blank? || !values.is_a?(Hash)
|
161
|
-
values.symbolize_keys!
|
162
|
-
values.each do |field, value|
|
163
|
-
next if value.blank?
|
164
|
-
send("#{field}=", value)
|
165
|
-
end
|
166
|
-
end
|
167
|
-
|
168
|
-
# Resets everything, your errors, record, cookies, and session. Basically "logs out" a user.
|
169
|
-
def destroy
|
170
|
-
before_destroy
|
171
|
-
|
172
|
-
errors.clear
|
173
|
-
@record = nil
|
174
|
-
|
175
|
-
after_destroy
|
176
|
-
|
177
|
-
true
|
178
|
-
end
|
179
|
-
|
180
|
-
# The errors in Authlogic work JUST LIKE ActiveRecord. In fact, it uses the exact same ActiveRecord errors class. Use it the same way:
|
181
|
-
#
|
182
|
-
# === Example
|
183
|
-
#
|
184
|
-
# class UserSession
|
185
|
-
# before_validation :check_if_awesome
|
186
|
-
#
|
187
|
-
# private
|
188
|
-
# def check_if_awesome
|
189
|
-
# errors.add(:login, "must contain awesome") if login && !login.include?("awesome")
|
190
|
-
# errors.add_to_base("You must be awesome to log in") unless record.awesome?
|
191
|
-
# end
|
192
|
-
# end
|
193
|
-
def errors
|
194
|
-
@errors ||= Errors.new(self)
|
195
|
-
end
|
196
|
-
|
197
|
-
# Attempts to find the record by params, then session, then cookie, and finally basic http auth. See the class level find method if you are wanting to use this to persist your session.
|
198
|
-
def find_record
|
199
|
-
if record
|
200
|
-
self.new_session = false
|
201
|
-
return record
|
202
|
-
end
|
203
|
-
|
204
|
-
find_with.each do |find_method|
|
205
|
-
if send("valid_#{find_method}?")
|
206
|
-
self.new_session = false
|
207
|
-
return record
|
208
|
-
end
|
209
|
-
end
|
210
|
-
nil
|
211
|
-
end
|
212
|
-
|
213
|
-
# Allows you to set a unique identifier for your session, so that you can have more than 1 session at a time. A good example when this might be needed is when you want to have a normal user session
|
214
|
-
# and a "secure" user session. The secure user session would be created only when they want to modify their billing information, or other sensitive information. Similar to me.com. This requires 2
|
215
|
-
# user sessions. Just use an id for the "secure" session and you should be good.
|
216
|
-
#
|
217
|
-
# You can set the id during initialization (see initialize for more information), or as an attribute:
|
218
|
-
#
|
219
|
-
# session.id = :my_id
|
220
|
-
#
|
221
|
-
# Just be sure and set your id before you save your session.
|
222
|
-
#
|
223
|
-
# Lastly, to retrieve your session with the id check out the find class method.
|
224
|
-
def id
|
225
|
-
@id
|
226
|
-
end
|
227
|
-
|
228
|
-
def inspect # :nodoc:
|
229
|
-
details = {}
|
230
|
-
case authenticating_with
|
231
|
-
when :unauthorized_record
|
232
|
-
details[:unauthorized_record] = "<protected>"
|
233
|
-
else
|
234
|
-
details[login_field.to_sym] = send(login_field)
|
235
|
-
details[password_field.to_sym] = "<protected>"
|
236
|
-
end
|
237
|
-
"#<#{self.class.name} #{details.inspect}>"
|
238
|
-
end
|
239
|
-
|
240
|
-
# Similar to ActiveRecord's new_record? Returns true if the session has not been saved yet.
|
241
|
-
def new_session?
|
242
|
-
new_session != false
|
243
|
-
end
|
244
|
-
|
245
|
-
def persisting # :nodoc:
|
246
|
-
return @persisting if defined?(@persisting)
|
247
|
-
@persisting = true
|
248
|
-
end
|
249
|
-
|
250
|
-
# Returns true if the session is being persisted. This is set to false if the session was found by the single_access_token, since logging in via a single access token should not remember the user in the
|
251
|
-
# session or the cookie.
|
252
|
-
def persisting?
|
253
|
-
persisting == true
|
254
|
-
end
|
255
|
-
|
256
|
-
def remember_me # :nodoc:
|
257
|
-
return @remember_me if defined?(@remember_me)
|
258
|
-
@remember_me = self.class.remember_me
|
259
|
-
end
|
260
|
-
|
261
|
-
# Accepts a boolean as a flag to remember the session or not. Basically to expire the cookie at the end of the session or keep it for "remember_me_until".
|
262
|
-
def remember_me=(value)
|
263
|
-
@remember_me = value
|
264
|
-
end
|
265
|
-
|
266
|
-
# Allows users to be remembered via a cookie.
|
267
|
-
def remember_me?
|
268
|
-
remember_me == true || remember_me == "true" || remember_me == "1"
|
269
|
-
end
|
270
|
-
|
271
|
-
# When to expire the cookie. See remember_me_for configuration option to change this.
|
272
|
-
def remember_me_until
|
273
|
-
return unless remember_me?
|
274
|
-
remember_me_for.from_now
|
275
|
-
end
|
276
|
-
|
277
|
-
# Creates / updates a new user session for you. It does all of the magic:
|
278
|
-
#
|
279
|
-
# 1. validates
|
280
|
-
# 2. sets session
|
281
|
-
# 3. sets cookie
|
282
|
-
# 4. updates magic fields
|
283
|
-
def save(&block)
|
284
|
-
result = nil
|
285
|
-
if valid?
|
286
|
-
# hooks
|
287
|
-
before_save
|
288
|
-
new_session? ? before_create : before_update
|
289
|
-
new_session? ? after_create : after_update
|
290
|
-
after_save
|
291
|
-
|
292
|
-
record.save_without_session_maintenance(false) if record.changed?
|
293
|
-
self.new_session = false
|
294
|
-
result = self
|
295
|
-
else
|
296
|
-
result = false
|
297
|
-
end
|
298
|
-
|
299
|
-
yield result if block_given?
|
300
|
-
result
|
301
|
-
end
|
302
|
-
|
303
|
-
# Same as save but raises an exception when authentication fails
|
304
|
-
def save!
|
305
|
-
result = save
|
306
|
-
raise SessionInvalid.new(self) unless result
|
307
|
-
result
|
308
|
-
end
|
309
|
-
|
310
|
-
# This lets you create a session by passing a single object of whatever you are authenticating. Let's say User. By passing a user object you are vouching for this user and saying you can guarantee
|
311
|
-
# this user is who he says he is, create a session for him.
|
312
|
-
#
|
313
|
-
# This is how persistence works in Authlogic. Authlogic grabs your cookie credentials, finds a user by those credentials, and then vouches for that user and creates a session. You can do this for just about
|
314
|
-
# anything, which comes in handy for those unique authentication methods. Do what you need to do to authenticate the user, guarantee he is who he says he is, then pass the object here. Authlogic will do its
|
315
|
-
# magic: create a session and cookie. Now when the user refreshes their session will be persisted by their session and cookie.
|
316
|
-
def unauthorized_record=(value)
|
317
|
-
self.authenticating_with = :unauthorized_record
|
318
|
-
@unauthorized_record = value
|
319
|
-
end
|
320
|
-
|
321
|
-
# Returns if the session is valid or not. Basically it means that a record could or could not be found. If the session is valid you will have a result when calling the "record" method. If it was unsuccessful
|
322
|
-
# you will not have a record.
|
323
|
-
def valid?
|
324
|
-
errors.clear
|
325
|
-
self.attempted_record = nil
|
326
|
-
|
327
|
-
before_validation
|
328
|
-
new_session? ? before_validation_on_create : before_validation_on_update
|
329
|
-
valid_credentials?
|
330
|
-
validate
|
331
|
-
|
332
|
-
if errors.empty?
|
333
|
-
new_session? ? after_validation_on_create : after_validation_on_update
|
334
|
-
after_validation
|
335
|
-
else
|
336
|
-
self.record = nil
|
337
|
-
end
|
338
|
-
|
339
|
-
attempted_record.save_without_session_maintenance(false) if attempted_record && attempted_record.changed?
|
340
|
-
self.attempted_record = nil
|
341
|
-
errors.empty?
|
342
|
-
end
|
343
|
-
|
344
|
-
# Tries to validate the session from information from a basic http auth, if it was provided.
|
345
|
-
def valid_http_auth?
|
346
|
-
controller.authenticate_with_http_basic do |login, password|
|
347
|
-
if !login.blank? && !password.blank?
|
348
|
-
send("#{login_field}=", login)
|
349
|
-
send("#{password_field}=", password)
|
350
|
-
return valid?
|
351
|
-
end
|
352
|
-
end
|
353
|
-
|
354
|
-
false
|
355
|
-
end
|
356
|
-
|
357
|
-
private
|
358
|
-
def controller
|
359
|
-
self.class.controller
|
360
|
-
end
|
361
|
-
|
362
|
-
# The goal with Authlogic is to feel as natural as possible. As a result, this method creates methods on the fly
|
363
|
-
# based on the configuration set. By default the configuration is based off of the columns names in the authenticating
|
364
|
-
# model. Thus allowing you to call user_session.username instead of user_session.login if you have a username column
|
365
|
-
# instead of a login column. Since class configuration can change during initialization it makes the most sense to enforce
|
366
|
-
# this configuration during the first initialization. At this point, all configuration should be set.
|
367
|
-
#
|
368
|
-
# Lastly, each method is defined individually to allow the user to provide their own "custom" method and this makes sure
|
369
|
-
# we don't replace their method.
|
370
|
-
def create_configurable_methods!
|
371
|
-
return if self.class.methods_configured == true
|
372
|
-
|
373
|
-
self.class.send(:alias_method, klass_name.demodulize.underscore.to_sym, :record)
|
374
|
-
self.class.send(:attr_writer, login_field) if !respond_to?("#{login_field}=")
|
375
|
-
self.class.send(:attr_reader, login_field) if !respond_to?(login_field)
|
376
|
-
self.class.send(:attr_writer, password_field) if !respond_to?("#{password_field}=")
|
377
|
-
self.class.send(:define_method, password_field) {} if !respond_to?(password_field)
|
378
|
-
|
379
|
-
self.class.class_eval <<-"end_eval", __FILE__, __LINE__
|
380
|
-
def #{login_field}_with_authentication_flag=(value)
|
381
|
-
self.authenticating_with = :password
|
382
|
-
self.#{login_field}_without_authentication_flag = value
|
383
|
-
end
|
384
|
-
alias_method_chain :#{login_field}=, :authentication_flag
|
385
|
-
|
386
|
-
def #{password_field}_with_authentication_flag=(value)
|
387
|
-
self.authenticating_with = :password
|
388
|
-
self.#{password_field}_without_authentication_flag = value
|
389
|
-
end
|
390
|
-
alias_method_chain :#{password_field}=, :authentication_flag
|
391
|
-
|
392
|
-
private
|
393
|
-
# The password should not be accessible publicly. This way forms using form_for don't fill the password with the attempted password. The prevent this we just create this method that is private.
|
394
|
-
def protected_#{password_field}
|
395
|
-
@#{password_field}
|
396
|
-
end
|
397
|
-
end_eval
|
398
|
-
|
399
|
-
self.class.methods_configured = true
|
400
|
-
end
|
401
|
-
|
402
|
-
def klass
|
403
|
-
self.class.klass
|
404
|
-
end
|
405
|
-
|
406
|
-
def klass_name
|
407
|
-
self.class.klass_name
|
408
|
-
end
|
409
|
-
|
410
|
-
def search_for_record(method, value)
|
411
|
-
klass.send(method, value)
|
412
|
-
end
|
413
|
-
|
414
|
-
def valid_credentials?
|
415
|
-
case authenticating_with
|
416
|
-
when :password
|
417
|
-
errors.add(login_field, I18n.t('error_messages.login_blank', :default => "can not be blank")) if send(login_field).blank?
|
418
|
-
errors.add(password_field, I18n.t('error_messages.password_blank', :default => "can not be blank")) if send("protected_#{password_field}").blank?
|
419
|
-
return false if errors.count > 0
|
420
|
-
|
421
|
-
self.attempted_record = search_for_record(find_by_login_method, send(login_field))
|
422
|
-
|
423
|
-
if attempted_record.blank?
|
424
|
-
errors.add(login_field, I18n.t('error_messages.login_not_found', :default => "does not exist"))
|
425
|
-
return false
|
426
|
-
end
|
427
|
-
|
428
|
-
unless attempted_record.send(verify_password_method, send("protected_#{password_field}"))
|
429
|
-
errors.add(password_field, I18n.t('error_messages.password_invalid', :default => "is not valid"))
|
430
|
-
return false
|
431
|
-
end
|
432
|
-
when :unauthorized_record
|
433
|
-
self.attempted_record = unauthorized_record
|
434
|
-
|
435
|
-
if attempted_record.blank?
|
436
|
-
errors.add_to_base(I18n.t('error_messages.blank_record', :default => "You can not login with a blank record"))
|
437
|
-
return false
|
438
|
-
end
|
439
|
-
|
440
|
-
if attempted_record.new_record?
|
441
|
-
errors.add_to_base(I18n.t('error_messages.new_record', :default => "You can not login with a new record"))
|
442
|
-
return false
|
443
|
-
end
|
444
|
-
end
|
445
|
-
|
446
|
-
self.record = attempted_record
|
447
|
-
valid_record?
|
448
|
-
end
|
449
|
-
|
450
|
-
def valid_record?
|
451
|
-
return true if disable_magic_states?
|
452
|
-
[:active, :approved, :confirmed].each do |required_status|
|
453
|
-
if record.respond_to?("#{required_status}?") && !record.send("#{required_status}?")
|
454
|
-
errors.add_to_base(I18n.t("error_messages.not_#{required_status}", :default => "Your account is not #{required_status}"))
|
455
|
-
return false
|
456
|
-
end
|
457
|
-
end
|
458
|
-
true
|
459
|
-
end
|
6
|
+
include Foundation
|
7
|
+
include Callbacks
|
8
|
+
|
9
|
+
# Included first so that the session resets itself to nil
|
10
|
+
include Timeout
|
11
|
+
|
12
|
+
# Included in a specific order so they are tried in this order when persisting
|
13
|
+
include Params
|
14
|
+
include Cookies
|
15
|
+
include Session
|
16
|
+
include HttpAuth
|
17
|
+
|
18
|
+
# Included in a specific order so magic states gets ran after a record is found
|
19
|
+
include Password
|
20
|
+
include UnauthorizedRecord
|
21
|
+
include MagicStates
|
22
|
+
|
23
|
+
include Activation
|
24
|
+
include ActiveRecordTrickery
|
25
|
+
include BruteForceProtection
|
26
|
+
include Existence
|
27
|
+
include Klass
|
28
|
+
include MagicColumns
|
29
|
+
include PerishableToken
|
30
|
+
include Persistence
|
31
|
+
include Scopes
|
32
|
+
include Id
|
33
|
+
include Validation
|
34
|
+
include PriorityRecord
|
460
35
|
end
|
461
36
|
end
|
462
37
|
end
|