global_session 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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