global_session 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,179 @@
1
+ = Introduction
2
+
3
+ HasGlobalSession enables multiple heterogeneous Web applications to share
4
+ session state in a cryptographically secure way, facilitating single sign-on
5
+ and enabling easier development of large-scale distributed applications that
6
+ make use of architectural strategies such as sharding or separation of concerns.
7
+
8
+ In other words: it lets semi-related Web apps share selected bits of session
9
+ state.
10
+
11
+ This plugin does not provide a complete solution for identity management. In
12
+ particular, it does not provide any of the following:
13
+
14
+ * <b>federation</b> -- aka cross-domain single sign-on -- use OpenID for that.
15
+
16
+ * <b>authentication</b> -- the application must authenticate the user.
17
+
18
+ * <b>authorization</b> -- the application is responsible for using the contents
19
+ of the global session to make authorization decisions.
20
+
21
+ * <b>secrecy</b> -- global session attributes can be signed but never encrypted;
22
+ protect against third-party snooping using SSL. Group secrecy is expensive;
23
+ if you don't want your users to see their session state, put it in a database,
24
+ or in an encrypted local session cookie.
25
+
26
+ * <b>replication</b> -- the authentication authorities must have some way to
27
+ share information about the database of users in order to authenticate
28
+ them and place identifying information into the global session.
29
+
30
+ * <b>single sign-out</b> -- the authorities must have some way to broadcast a
31
+ notification when sessions are invalidated; they can override the default
32
+ Directory implementation to do realtime revocation checking.
33
+
34
+ = Example
35
+
36
+ 1) Create a basic config file and edit it to suit your needs:
37
+ $ script/generate global_session_config mycoolapp.com
38
+
39
+ 2) Create an authentication authority:
40
+ $ script/generate global_session_authority mycoolapp
41
+
42
+ 3) Declare that some or all of your controllers will use the global session:
43
+ class ApplicationController < ActionController::Base
44
+ has_global_session
45
+ end
46
+
47
+ 4) Make use of the global session hash in your controllers:
48
+ global_session['user'] = @user.id
49
+ ...
50
+ @current_user = User.find(global_session['user'])
51
+
52
+ 5) For easier programming, enable seamless integration with the local session:
53
+ (in global_session.yml)
54
+ common:
55
+ integrated: true
56
+
57
+ (in your controllers)
58
+ session['user'] = @user.id #goes to the global session
59
+ session['local_thingie'] = @thingie.id #goes to the local session
60
+
61
+ = Global Session Contents
62
+
63
+ Global session state is stored as a cookie in the user's browser. The cookie
64
+ is a Zlib-compressed JSON dictionary containing the following stuff:
65
+ * session metadata (UUID, created-at, expires-at, signing-authority)
66
+ * signed session attributes (e.g. the authenticated user ID)
67
+ * insecure session attributes (e.g. the last-visited URL)
68
+ * a cryptographic signature of the metadata and signed attributes
69
+
70
+ The global session is unserialized and its signature is verified whenever
71
+ a controller asks for one of its attributes. The cookie's value is updated
72
+ whenever attributes change. As an optimization, the signature is only
73
+ recomputed when the metadata or signed attributes have changed; insecure
74
+ attributes can change "for free."
75
+
76
+ Because the security properties of attributes can vary, HasGlobalSession
77
+ requires all _possible_ attributes to be declared up-front in the config
78
+ file. The 'attributes' section of the config file defines the _schema_
79
+ for the global session: which attributes can be used, which can be trusted
80
+ to make authorization decisions (because they are signed), and are insecure
81
+ and therefore act only as "hints" about the session.
82
+
83
+ Since the session is serialized as JSON, only a limited range of object
84
+ types can be stored in it: strings, numbers, lists, hashes and other Ruby
85
+ primitives. Ruby booleans (true/false) do not translate well into JSON
86
+ and should be avoided.
87
+
88
+ = Detailed Information
89
+
90
+ == Global Session Domain
91
+
92
+ We refer to collection of _all_ Web application instances capable of using the
93
+ global session as the "domain." The global session domain may consist of any
94
+ number of distinct nodes, possibly hidden behind load balancers or proxies.
95
+ The nodes within the domain may all be running the same Rails application,
96
+ or they may be running different codebases that represent different parts of
97
+ a distributed application. (They may also be using app frameworks other than
98
+ Rails.)
99
+
100
+ The only constraint imposed by HasGlobalSession is that all nodes within the
101
+ domain must have end-user-facing URLs within the same second-level DNS domain.
102
+ This is due to limitations imposed by the HTTP cookie mechanism: for privacy
103
+ reasons, cookies will only be sent to nodes within the same domain as the
104
+ node that first created them.
105
+
106
+ For example, in my HasGlobalSession configuration file I might specify that my
107
+ cookie's domain is "example.com". My app nodes at app1.example.com and
108
+ app2.example.com would be part of the global session domain, but my business
109
+ partner's application at app3.partner.com could not participate.
110
+
111
+ == Authorities and Relying Parties
112
+
113
+ A node that can create or update the global session is said to be an "authority"
114
+ (because it's trusted by other parties to make assertions about global session
115
+ state). An application that can read the global session is said to be a "relying
116
+ party." In practice, every application is a relying party, but not all of them
117
+ need to be authorities.
118
+
119
+ There is an RSA key pair associated with each authority. The authority's
120
+ public key is distribued to all relying parties, but the private key must
121
+ remain a secret to that authority (which may consist of many individual
122
+ nodes).
123
+
124
+ This system allows for significant flexibility when configuring a distributed
125
+ app's global session. There must be at least one authority, but for many apps
126
+ one authority (plus an arbitrary number of relying parties, which do not need
127
+ a key pair) will be sufficient.
128
+
129
+ In general, two systems should be part of the same authority if there is no
130
+ trust boundary between them -- that is to say, trust between the two systems
131
+ is unlimited in both directions.
132
+
133
+ Here are some reasons you might consider dividing your systems into different
134
+ authorities:
135
+ * beta/staging system vs. production system
136
+ * system hosted by a third party vs. system hosted internally
137
+ * e-commerce node vs. storefront node vs. admin node
138
+
139
+ == The Directory
140
+
141
+ The Directory is a Ruby object instantiated by HasGlobalSession in order to
142
+ perform lookups of public and private keys. Given an authority name (as found
143
+ in a session cookie), the Directory can find the corresponding public key.
144
+
145
+ If the local system is an authority itself, #local_authority_name will
146
+ return non-nil and #private_key will return a private key suitable for
147
+ signing session attributes.
148
+
149
+ The Directory implementation included with HasGlobalSession uses the filesystem
150
+ as the backing store for its key pairs. Its #initialize method accepts a
151
+ filesystem path that will be searched for files containing PEM-encoded public
152
+ and private keys (the same format used by OpenSSH). This simple Directory
153
+ implementation relies on the following conventions:
154
+ * Public keys have a *.pub extension.
155
+ * Private keys have a *.key extension.
156
+ * If a node is an authority, then one (and *only* one) *.key file should exist.
157
+ * The local node's authority name is inferred from the name of the private key
158
+ file.
159
+
160
+ When used with a Rails app, HasGlobalSession expects to find its keystore in
161
+ config/authorities. You can use the global_session generator to create new key
162
+ pairs. Remember never to check a *.key file into a public repository!! (*.pub
163
+ files can be checked into source control and distributed freely.)
164
+
165
+ If you wish all of the systems to stop trusting an authority, simply delete
166
+ its public key from config/authorities and re-deploy your app.
167
+
168
+ === Implementing Your Own Directory Provider
169
+
170
+ To replace or enhance the built-in Directory, simply create a new class that
171
+ extends Directory and put the class somewhere in your app (the lib directory
172
+ is a good choice). In the HasGlobalSession configuration file, specify the
173
+ class name of the directory under the 'common' section, like so:
174
+
175
+ common:
176
+ integrated: true
177
+ directory: MyCoolDirectory
178
+
179
+ Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
@@ -0,0 +1,37 @@
1
+ # -*- mode: ruby; encoding: utf-8 -*-
2
+
3
+ require 'rubygems'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.required_rubygems_version = nil if s.respond_to? :required_rubygems_version=
7
+ s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
8
+
9
+ s.name = 'global_session'
10
+ s.version = '0.9.0'
11
+ s.date = '2010-12-07'
12
+
13
+ s.authors = ['Tony Spataro']
14
+ s.email = 'code@tracker.xeger.net'
15
+ s.homepage= 'http://github.com/xeger/global_session'
16
+
17
+ s.summary = %q{Secure single-domain session sharing plugin for Rails.}
18
+ s.description = %q{This plugin for Rails allows several web apps in an authentication domain to share session state, facilitating single sign-on in a distributed web app. It only provides session sharing and does not concern itself with authentication or replication of the user database.}
19
+
20
+ s.add_runtime_dependency('uuidtools', [">= 1.0.7"])
21
+ s.add_runtime_dependency('json', [">= 1.1.7"])
22
+ s.add_runtime_dependency('rack-contrib', [">= 1.0"])
23
+
24
+ s.add_development_dependency('rake', ["~> 0.8.7"])
25
+ s.add_development_dependency('ruby-debug', ["~> 0.10.3"])
26
+ s.add_development_dependency('rspec', [">= 1.3.0"])
27
+ s.add_development_dependency('flexmock', [">= 0.8.6"])
28
+ s.add_development_dependency('actionpack', [">= 2.1.2"])
29
+
30
+ basedir = File.dirname(__FILE__)
31
+ candidates = ['global_session.gemspec', 'init.rb', 'MIT-LICENSE', 'README.rdoc'] +
32
+ Dir['lib/**/*'] +
33
+ Dir['rails/**/*'] +
34
+ Dir['rails/**/*'] +
35
+ Dir['rails_generators/**/*']
36
+ s.files = candidates.sort
37
+ end
data/init.rb ADDED
@@ -0,0 +1,4 @@
1
+ # Stub to invoke real init.rb when GlobalSession is installed as a vendored
2
+ # Rails plugin.
3
+ basedir = File.dirname(__FILE__)
4
+ require File.join(basedir, 'rails', 'init')
@@ -0,0 +1,126 @@
1
+ module GlobalSession
2
+ # Central point of access for GlobalSession configuration information. This is
3
+ # mostly a very thin wrapper around the serialized hash written to the YAML config
4
+ # file.
5
+ #
6
+ # The configuration is stored as a set of nested hashes and accessed by the code
7
+ # using hash lookup; for example, we might ask for +Configuration['cookie']['domain']+
8
+ # if we wanted to know which domain the cookie should be set for.
9
+ #
10
+ # The following settings are supported:
11
+ # * attributes
12
+ # * signed
13
+ # * insecure
14
+ # * integrated
15
+ # * ephemeral
16
+ # * timeout
17
+ # * renew
18
+ # * authority
19
+ # * trust
20
+ # * directory
21
+ # * cookie
22
+ # * name
23
+ # * domain
24
+ #
25
+ # === Config Environments
26
+ # The operational environment of global_session defines which section
27
+ # of the configuration file it gets its settings from. When used with
28
+ # a web app, the environment should be set to the same environment as
29
+ # the web app. (If using Rails integration, this happens for you
30
+ # automatically.)
31
+ #
32
+ # === Environment-Specific Settings
33
+ # The top level of keys in the configuration hash are special; they provide different
34
+ # sections of settings that apply in different environments. For instance, a Rails
35
+ # application might have one set of settings that apply in the development environment;
36
+ # these would appear under +Configuration['development']+. Another set of settings would
37
+ # apply in the production environment and would appear under +Configuration['production']+.
38
+ #
39
+ # === Common Settings
40
+ # In addition to having one section for each operating environment, the configuration
41
+ # file can specify a 'common' section for settings that apply
42
+ #
43
+ # === Lookup Mechanism
44
+ # When the code asks for +Configuration['foo']+, we first check whether the current
45
+ # environment's config section has a value for foo. If one is found, we return that.
46
+ #
47
+ # If no environment-specific setting is found, we check the 'common' section and return
48
+ # the value found there.
49
+ #
50
+ # === Config File Location
51
+ # The name and location of the config file depend on the Web framework with which
52
+ # you are integrating; see GlobalSession::Rails for more information.
53
+ #
54
+ class Configuration
55
+ # Create a new Configuration objectt
56
+ #
57
+ # === Parameters
58
+ # config_File(String):: Absolute filesystem path to the configuration file
59
+ # environment(String):: Config file section from which to read settings
60
+ #
61
+ # === Raise
62
+ # MissingConfiguration:: if config file is missing or unreadable
63
+ # TypeError:: if config file does not contain a YAML-serialized Hash
64
+ def initialize(config_file, environment)
65
+ @config_file = config_file
66
+ @environment = environment
67
+ raise MissingConfiguration, "Missing or unreadable configuration file" unless File.readable?(@config_file)
68
+ @config = YAML.load(File.read(@config_file))
69
+ raise TypeError, "#{config_file} must contain a Hash!" unless Hash === @config
70
+ validate
71
+ end
72
+
73
+ # Reader for configuration elements. The reader first checks
74
+ # the current environment's settings section for the named
75
+ # value; if not found, it checks the common settings section.
76
+ #
77
+ # === Parameters
78
+ # key(String):: Name of configuration element to retrieve
79
+ #
80
+ # === Return
81
+ # value(String):: the value of the configuration element
82
+ def [](key)
83
+ get(key, true)
84
+ end
85
+
86
+ def validate # :nodoc
87
+ ['attributes/signed', 'integrated', 'cookie/name',
88
+ 'timeout'].each {|k| validate_presence_of k}
89
+ end
90
+
91
+ protected
92
+
93
+ # Helper method to check the presence of a key. Used in #validate.
94
+ #
95
+ # === Parameters
96
+ # key(String):: key name; for nested hashes, separate keys with /
97
+ #
98
+ # === Return
99
+ # true always
100
+ def validate_presence_of(key)
101
+ elements = key.split '/'
102
+ object = get(elements.shift, false)
103
+ elements.each do |element|
104
+ object = object[element] if object
105
+ if object.nil?
106
+ msg = "#{File.basename(@config_file)} does not specify required element #{elements.map { |x| "['#{x}']"}.join('')}"
107
+ raise MissingConfiguration, msg
108
+ end
109
+ end
110
+ true
111
+ end
112
+
113
+ private
114
+
115
+ def get(key, validated) # :nodoc
116
+ if @config.has_key?(@environment) &&
117
+ @config[@environment].has_key?(key)
118
+ return @config[@environment][key]
119
+ else
120
+ @config['common'][key]
121
+ end
122
+ rescue NoMethodError
123
+ raise MissingConfiguration, "Configuration key '#{key}' not found"
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,103 @@
1
+ module GlobalSession
2
+ # The global session directory, which provides some lookup and decision services
3
+ # to instances of Session.
4
+ #
5
+ # The default implementation is simplistic, but should be suitable for most applications.
6
+ # Directory is designed to be specialized via subclassing. To override the behavior to
7
+ # suit your needs, simply create a subclass of Directory and add a configuration file
8
+ # setting to specify the class name of your implementation:
9
+ #
10
+ # common:
11
+ # directory: MyCoolDirectory
12
+ #
13
+ #
14
+ # === The Authority Keystore
15
+ # Directory uses a filesystem directory as a backing store for RSA
16
+ # public keys of global session authorities. The directory should
17
+ # contain one or more +*.pub+ files containing OpenSSH-format public
18
+ # RSA keys. The name of the pub file determines the name of the
19
+ # authority it represents.
20
+ #
21
+ # === The Local Authority
22
+ # Directory will infer the name of the local authority (if any) by
23
+ # looking for a private-key file in the keystore. If a +*.key+ file
24
+ # is found, then its name is taken to be the name of the local
25
+ # authority and all GlobalSessions created will be signed by that
26
+ # authority's private key.
27
+ #
28
+ # If more than one key file is found, Directory will raise an error
29
+ # at initialization time.
30
+ #
31
+ class Directory
32
+ attr_reader :configuration, :authorities, :private_key, :local_authority_name
33
+
34
+ # Create a new Directory.
35
+ #
36
+ # === Parameters
37
+ # keystore_directory(String):: Absolute path to authority keystore
38
+ #
39
+ # ===Raise
40
+ # ConfigurationError:: if too many or too few keys are found, or if *.key/*.pub files are malformatted
41
+ def initialize(configuration, keystore_directory)
42
+ @configuration = configuration
43
+ certs = Dir[File.join(keystore_directory, '*.pub')]
44
+ keys = Dir[File.join(keystore_directory, '*.key')]
45
+ raise ConfigurationError, "Excepted 0 or 1 key files, found #{keys.size}" unless [0, 1].include?(keys.size)
46
+
47
+ @authorities = {}
48
+ certs.each do |cert_file|
49
+ basename = File.basename(cert_file)
50
+ authority = basename[0...(basename.rindex('.'))] #chop trailing .ext
51
+ @authorities[authority] = OpenSSL::PKey::RSA.new(File.read(cert_file))
52
+ raise ConfigurationError, "Expected #{basename} to contain an RSA public key" unless @authorities[authority].public?
53
+ end
54
+
55
+ if (authority_name = @configuration['authority'])
56
+ key_file = keys.detect { |kf| kf =~ /#{authority_name}.key$/ }
57
+ raise ConfigurationError, "Key file #{authority_name}.key not found" unless key_file
58
+ @private_key = OpenSSL::PKey::RSA.new(File.read(key_file))
59
+ raise ConfigurationError, "Expected #{key_file} to contain an RSA private key" unless @private_key.private?
60
+ @local_authority_name = authority_name
61
+ end
62
+ end
63
+
64
+ # Determine whether this system trusts a particular authority based on
65
+ # the trust settings specified in Configuration.
66
+ #
67
+ # === Parameters
68
+ # authority(String):: The name of the authority
69
+ #
70
+ # === Return
71
+ # trusted(true|false):: whether the local system trusts sessions signed by the specified authority
72
+ def trusted_authority?(authority)
73
+ @configuration['trust'].include?(authority)
74
+ end
75
+
76
+ # Determine whether the given session UUID is valid. The default implementation only considers
77
+ # a session to be invalid if its expired_at timestamp is in the past. Custom implementations
78
+ # might want to consider other factors, such as whether the user has signed out of this node
79
+ # or another node (perhaps using some sort of centralized lookup or single sign-out mechanism).
80
+ #
81
+ # === Parameters
82
+ # uuid(String):: Global session UUID
83
+ # expired_at(Time):: When the session expired (or will expire)
84
+ #
85
+ # === Return
86
+ # valid(true|false):: whether the specified session is valid
87
+ def valid_session?(uuid, expired_at)
88
+ expired_at > Time.now
89
+ end
90
+
91
+ # Callback used by Session objects to report when the application code calls
92
+ # #invalidate! on them. The default implementation of this method does nothing.
93
+ #
94
+ # uuid(String):: Global session UUID
95
+ # expired_at(Time):: When the session expired
96
+ #
97
+ # === Return
98
+ # true:: Always returns true
99
+ def report_invalid_session(uuid, expired_at)
100
+ true
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,70 @@
1
+ module GlobalSession
2
+ # Various encoding (not encryption!) techniques used by the global session plugin.
3
+ #
4
+ module Encoding
5
+ # JSON serializer, used to serialize Hash objects in a form suitable
6
+ # for stuffing into a cookie.
7
+ #
8
+ class JSON
9
+ # Unserialize JSON to Hash.
10
+ #
11
+ # === Parameters
12
+ # json(String):: A well-formed JSON document
13
+ #
14
+ # === Return
15
+ # value(Hash):: An unserialized Ruby Hash
16
+ def self.load(json)
17
+ ::JSON.load(json)
18
+ end
19
+
20
+ # Serialize Hash to JSON document.
21
+ #
22
+ # === Parameters
23
+ # value(Hash):: The hash to be serialized
24
+ #
25
+ # === Return
26
+ # json(String):: A JSON-serialized representation of +value+
27
+ def self.dump(object)
28
+ return object.to_json
29
+ end
30
+ end
31
+
32
+ # Implements URL encoding, but without newlines, and using '-' and '_' as
33
+ # the 62nd and 63rd symbol instead of '+' and '/'. This makes for encoded
34
+ # values that can be easily stored in a cookie; however, they cannot
35
+ # be used in a URL query string without URL-escaping them since they
36
+ # will contain '=' characters.
37
+ #
38
+ # This scheme is almost identical to the scheme "Base 64 Encoding with URL
39
+ # and Filename Safe Alphabet," described in RFC4648, with the exception that
40
+ # this scheme preserves the '=' padding characters due to limitations of
41
+ # Ruby's built-in base64 encoding routines.
42
+ class Base64Cookie
43
+ # Decode a B64cookie-encoded string.
44
+ #
45
+ # === Parameters
46
+ # encoded(String):: The encoded string
47
+ #
48
+ # === Return
49
+ # decoded(String):: The decoded result, which may contain nonprintable bytes
50
+ def self.load(string)
51
+ tr = string.tr('-_', '+/')
52
+ return tr.unpack('m')[0]
53
+ end
54
+
55
+ # Encode a Ruby (ASCII or binary) string.
56
+ #
57
+ # === Parameters
58
+ # decoded(String):: The raw string to be encoded
59
+ #
60
+ # === Return
61
+ # encoded(String):: The B64cookie-encoded result.
62
+ def self.dump(object)
63
+ raw = [object].pack('m')
64
+ raw.tr!('+/', '-_')
65
+ raw.gsub!("\n", '')
66
+ return raw
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,123 @@
1
+ module GlobalSession
2
+ # Helper class that enables the end user to treat the global and local session as if
3
+ # they were the same object. This is accomplished by implementing approximately the
4
+ # same interface as a Hash, and dispatching to one or the other session object depending
5
+ # on various factors.
6
+ #
7
+ # This class isn't intended to be used directly by the end user. Instead, set integrated: true
8
+ # in the configuration file and the Web framework integration code will manage an integrated
9
+ # session object for you, as well as overriding the framework's default session accessor to
10
+ # return an integrated session instead.
11
+ #
12
+ # When using an integrated session, you can always get to the underlying objects by
13
+ # using the #local and #global readers of this class.
14
+ #
15
+ class IntegratedSession
16
+ # Return the local-session objects, whose type may vary depending on the Web framework.
17
+ attr_reader :local
18
+
19
+ # Return the global-session object.
20
+ attr_reader :global
21
+
22
+ # Construct a new integrated session.
23
+ #
24
+ # === Parameters
25
+ # local(Object):: Local session that acts like a Hash
26
+ # global(Session):: Session
27
+ def initialize(local, global)
28
+ @local = local
29
+ @global = global
30
+ end
31
+
32
+ # Retrieve a value from the global session if the supplied key is supported by
33
+ # the global session, else retrieve it from the local session.
34
+ #
35
+ # === Parameters
36
+ # key(String):: the key
37
+ #
38
+ # === Return
39
+ # value(Object):: The value associated with +key+, or nil if +key+ is not present
40
+ def [](key)
41
+ key = key.to_s
42
+ if @global.supports_key?(key)
43
+ @global[key]
44
+ else
45
+ @local[key]
46
+ end
47
+ end
48
+
49
+ # Set a value in the global session (if the supplied key is supported) or the local
50
+ # session otherwise.
51
+ #
52
+ # === Parameters
53
+ # key(String):: The key to set
54
+ # value(Object):: The value to set
55
+ #
56
+ # === Return
57
+ # value(Object):: Always returns the value that was set
58
+ def []=(key, value)
59
+ key = key.to_s
60
+ if @global.supports_key?(key)
61
+ @global[key] = value
62
+ else
63
+ @local[key] = value
64
+ end
65
+
66
+ return value
67
+ end
68
+
69
+ # Determine whether the global or local session contains a value with the specified key.
70
+ #
71
+ # === Parameters
72
+ # key(String):: The name of the key
73
+ #
74
+ # === Return
75
+ # contained(true|false):: Whether the session currently has a value for the specified key.
76
+ def has_key?(key)
77
+ key = key.to_s
78
+ @global.has_key?(key) || @local.has_key?(key)
79
+ end
80
+
81
+ # Return the keys that are currently present in either the global or local session.
82
+ #
83
+ # === Return
84
+ # keys(Array):: List of keys contained in the global or local session.
85
+ def keys
86
+ @global.keys + @local.keys
87
+ end
88
+
89
+ # Return the values that are currently present in the global or local session.
90
+ #
91
+ # === Return
92
+ # values(Array):: List of values contained in the global or local session.
93
+ def values
94
+ @global.values + @local.values
95
+ end
96
+
97
+ # Iterate over each key/value pair in both the global and local session.
98
+ #
99
+ # === Block
100
+ # An iterator which will be called with each key/value pair
101
+ #
102
+ # === Return
103
+ # Returns the value of the last expression evaluated by the block
104
+ def each_pair(&block)
105
+ @global.each_pair(&block)
106
+ @local.each_pair(&block)
107
+ end
108
+
109
+ def respond_to?(meth) # :nodoc:
110
+ return @global.respond_to?(meth) || @local.respond_to?(meth) || super
111
+ end
112
+
113
+ def method_missing(meth, *args) # :nodoc:
114
+ if @global.respond_to?(meth)
115
+ @global.__send__(meth, *args)
116
+ elsif @local.respond_to?(meth)
117
+ @local.__send__(meth, *args)
118
+ else
119
+ super
120
+ end
121
+ end
122
+ end
123
+ end