has_global_session 0.9.5 → 1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +0 -6
- data/has_global_session.gemspec +2 -2
- data/lib/has_global_session/configuration.rb +85 -2
- data/lib/has_global_session/directory.rb +63 -0
- data/lib/has_global_session/encoding.rb +33 -0
- data/lib/has_global_session/global_session.rb +123 -16
- data/lib/has_global_session/integrated_session.rb +65 -12
- data/lib/has_global_session/rails/action_controller_instance_methods.rb +54 -6
- data/lib/has_global_session.rb +37 -0
- metadata +4 -5
data/README.rdoc
CHANGED
@@ -176,10 +176,4 @@ class name of the directory under the 'common' section, like so:
|
|
176
176
|
integrated: true
|
177
177
|
directory: MyCoolDirectory
|
178
178
|
|
179
|
-
= To-Do
|
180
|
-
|
181
|
-
* Option to auto-renew session
|
182
|
-
|
183
|
-
* Implement single sign-out via redirect
|
184
|
-
|
185
179
|
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
|
data/has_global_session.gemspec
CHANGED
@@ -7,8 +7,8 @@ spec = Gem::Specification.new do |s|
|
|
7
7
|
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
8
|
|
9
9
|
s.name = 'has_global_session'
|
10
|
-
s.version = '0
|
11
|
-
s.date = '2010-07-
|
10
|
+
s.version = '1.0'
|
11
|
+
s.date = '2010-07-27'
|
12
12
|
|
13
13
|
s.authors = ['Tony Spataro']
|
14
14
|
s.email = 'code@tracker.xeger.net'
|
@@ -1,16 +1,98 @@
|
|
1
1
|
module HasGlobalSession
|
2
|
+
# Central point of access for HasGlobalSession 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
|
+
# === Environment-Specific Settings
|
26
|
+
# The top level of keys in the configuration hash are special; they provide different
|
27
|
+
# sections of settings that apply in different environments. For instance, a Rails
|
28
|
+
# application might have one set of settings that apply in the development environment;
|
29
|
+
# these would appear under +Configuration['development']+. Another set of settings would
|
30
|
+
# apply in the production environment and would appear under +Configuration['production']+.
|
31
|
+
#
|
32
|
+
# === Common Settings
|
33
|
+
# In addition to having one section for each operating environment, the configuration
|
34
|
+
# file can specify a 'common' section for settings that apply
|
35
|
+
#
|
36
|
+
# === Lookup Mechanism
|
37
|
+
# When the code asks for +Configuration['foo']+, we first check whether the current
|
38
|
+
# environment's config section has a value for foo. If one is found, we return that.
|
39
|
+
#
|
40
|
+
# If no environment-specific setting is found, we check the 'common' section and return
|
41
|
+
# the value found there.
|
42
|
+
#
|
43
|
+
# === Config File Location
|
44
|
+
# The name and location of the config file depend on the Web framework with which
|
45
|
+
# you are integrating; see HasGlobalSession::Rails for more information.
|
46
|
+
#
|
2
47
|
module Configuration
|
48
|
+
# Reader for the environment module-attribute.
|
49
|
+
#
|
50
|
+
# === Return
|
51
|
+
# env(String):: The current configuration environment
|
3
52
|
def self.environment; @environment; end
|
53
|
+
|
54
|
+
# Writer for the environment module-attribute.
|
55
|
+
#
|
56
|
+
# === Parameters
|
57
|
+
# value(String):: Configuration environment from which settings should be read
|
58
|
+
#
|
59
|
+
# === Return
|
60
|
+
# env(String):: The new configuration environment
|
4
61
|
def self.environment=(value); @environment = value; end
|
5
62
|
|
63
|
+
# Reader for the config_file module-attribute.
|
64
|
+
#
|
65
|
+
# === Return
|
66
|
+
# file(String):: Absolute path to configuration file
|
6
67
|
def self.config_file; @config_file; end
|
68
|
+
|
69
|
+
# Writer for the config_file module-attribute.
|
70
|
+
#
|
71
|
+
# === Parameters
|
72
|
+
# value(String):: Absolute path to configuration file
|
73
|
+
#
|
74
|
+
# === Return
|
75
|
+
# env(String):: The new path to the configuration file
|
7
76
|
def self.config_file=(value); @config_file= value; end
|
8
77
|
|
78
|
+
# Reader for configuration elements. The reader first checks
|
79
|
+
# the current environment's settings section for the named
|
80
|
+
# value; if not found, it checks the common settings section.
|
81
|
+
#
|
82
|
+
# === Parameters
|
83
|
+
# name(Type):: Description
|
84
|
+
#
|
85
|
+
# === Return
|
86
|
+
# name(Type):: Description
|
87
|
+
#
|
88
|
+
# === Raise
|
89
|
+
# MissingConfiguration:: if config file location is unset, environment is unset, or config file is missing
|
90
|
+
# TypeError:: if config file does not contain a YAML-serialized Hash
|
9
91
|
def self.[](key)
|
10
92
|
get(key, true)
|
11
93
|
end
|
12
94
|
|
13
|
-
def self.validate
|
95
|
+
def self.validate # :nodoc
|
14
96
|
['attributes/signed', 'integrated', 'cookie/name', 'timeout'].each do |path|
|
15
97
|
elements = path.split '/'
|
16
98
|
object = get(elements.shift, false)
|
@@ -25,7 +107,8 @@ module HasGlobalSession
|
|
25
107
|
end
|
26
108
|
|
27
109
|
private
|
28
|
-
|
110
|
+
|
111
|
+
def self.get(key, validated) # :nodoc
|
29
112
|
unless @config
|
30
113
|
raise MissingConfiguration, "config_file is nil; cannot read configuration" unless config_file
|
31
114
|
raise MissingConfiguration, "environment is nil; must be specified" unless environment
|
@@ -1,7 +1,43 @@
|
|
1
1
|
module HasGlobalSession
|
2
|
+
# The global session directory, which provides some lookup and decision services
|
3
|
+
# to instances of GlobalSession.
|
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
|
+
#
|
2
31
|
class Directory
|
3
32
|
attr_reader :authorities, :private_key, :local_authority_name
|
4
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
|
5
41
|
def initialize(keystore_directory)
|
6
42
|
certs = Dir[File.join(keystore_directory, '*.pub')]
|
7
43
|
keys = Dir[File.join(keystore_directory, '*.key')]
|
@@ -24,14 +60,41 @@ module HasGlobalSession
|
|
24
60
|
end
|
25
61
|
end
|
26
62
|
|
63
|
+
# Determine whether this system trusts a particular authority based on
|
64
|
+
# the trust settings specified in Configuration.
|
65
|
+
#
|
66
|
+
# === Parameters
|
67
|
+
# authority(String):: The name of the authority
|
68
|
+
#
|
69
|
+
# === Return
|
70
|
+
# trusted(true|false):: whether the local system trusts sessions signed by the specified authority
|
27
71
|
def trusted_authority?(authority)
|
28
72
|
Configuration['trust'].include?(authority)
|
29
73
|
end
|
30
74
|
|
75
|
+
# Determine whether the given session UUID is valid. The default implementation only considers
|
76
|
+
# a session to be invalid if its expired_at timestamp is in the past. Custom implementations
|
77
|
+
# might want to consider other factors, such as whether the user has signed out of this node
|
78
|
+
# or another node (perhaps using some sort of centralized lookup or single sign-out mechanism).
|
79
|
+
#
|
80
|
+
# === Parameters
|
81
|
+
# uuid(String):: Global session UUID
|
82
|
+
# expired_at(Time):: When the session expired (or will expire)
|
83
|
+
#
|
84
|
+
# === Return
|
85
|
+
# valid(true|false):: whether the specified session is valid
|
31
86
|
def valid_session?(uuid, expired_at)
|
32
87
|
expired_at > Time.now
|
33
88
|
end
|
34
89
|
|
90
|
+
# Callback used by GlobalSession objects to report when the application code calls
|
91
|
+
# #invalidate! on them. The default implementation of this method does nothing.
|
92
|
+
#
|
93
|
+
# uuid(String):: Global session UUID
|
94
|
+
# expired_at(Time):: When the session expired
|
95
|
+
#
|
96
|
+
# === Return
|
97
|
+
# true:: Always returns true
|
35
98
|
def report_invalid_session(uuid, expired_at)
|
36
99
|
true
|
37
100
|
end
|
@@ -1,10 +1,29 @@
|
|
1
1
|
module HasGlobalSession
|
2
|
+
# Various encoding (not encryption!) techniques used by the global session plugin.
|
3
|
+
#
|
2
4
|
module Encoding
|
5
|
+
# JSON serializer, used to serialize Hash objects in a form suitable
|
6
|
+
# for stuffing into a cookie.
|
7
|
+
#
|
3
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
|
4
16
|
def self.load(json)
|
5
17
|
::JSON.load(json)
|
6
18
|
end
|
7
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+
|
8
27
|
def self.dump(object)
|
9
28
|
return object.to_json
|
10
29
|
end
|
@@ -21,11 +40,25 @@ module HasGlobalSession
|
|
21
40
|
# this scheme preserves the '=' padding characters due to limitations of
|
22
41
|
# Ruby's built-in base64 encoding routines.
|
23
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
|
24
50
|
def self.load(string)
|
25
51
|
tr = string.tr('-_', '+/')
|
26
52
|
return tr.unpack('m')[0]
|
27
53
|
end
|
28
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.
|
29
62
|
def self.dump(object)
|
30
63
|
raw = [object].pack('m')
|
31
64
|
raw.tr!('+/', '-_')
|
@@ -6,15 +6,34 @@ require 'zlib'
|
|
6
6
|
require 'uuidtools'
|
7
7
|
|
8
8
|
module HasGlobalSession
|
9
|
+
# Ladies and gentlemen: the one and only, star of the show, GLOBAL SESSION!
|
10
|
+
#
|
11
|
+
# GlobalSession is designed to act as much like a Hash as possible. You can use
|
12
|
+
# most of the methods you would use with Hash: [], has_key?, each, etc. It has a
|
13
|
+
# few additional methods that are specific to itself, mostly involving whether
|
14
|
+
# it's expired, valid, supports a certain key, etc.
|
15
|
+
#
|
9
16
|
class GlobalSession
|
10
17
|
attr_reader :id, :authority, :created_at, :expired_at, :directory
|
11
18
|
|
19
|
+
# Create a new global session object.
|
20
|
+
#
|
21
|
+
# === Parameters
|
22
|
+
# directory(Directory):: directory implementation that the session should use for various operations
|
23
|
+
# cookie(String):: Optional, serialized global session cookie. If none is supplied, a new session is created.
|
24
|
+
# valid_signature_digest(String):: Optional, already-trusted signature. If supplied, the expensive RSA-verify operation will be skipped if the cookie's signature matches the value supplied.
|
25
|
+
#
|
26
|
+
# ===Raise
|
27
|
+
# InvalidSession:: if the session contained in the cookie has been invalidated
|
28
|
+
# ExpiredSession:: if the session contained in the cookie has expired
|
29
|
+
# MalformedCookie:: if the cookie was corrupt or malformed
|
30
|
+
# SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
|
12
31
|
def initialize(directory, cookie=nil, valid_signature_digest=nil)
|
13
32
|
@schema_signed = Set.new((Configuration['attributes']['signed']))
|
14
33
|
@schema_insecure = Set.new((Configuration['attributes']['insecure']))
|
15
34
|
@directory = directory
|
16
35
|
|
17
|
-
if cookie
|
36
|
+
if cookie && !cookie.empty?
|
18
37
|
load_from_cookie(cookie, valid_signature_digest)
|
19
38
|
elsif @directory.local_authority_name
|
20
39
|
create_from_scratch
|
@@ -23,10 +42,21 @@ module HasGlobalSession
|
|
23
42
|
end
|
24
43
|
end
|
25
44
|
|
45
|
+
# Determine whether the session is valid. This method simply delegates to the
|
46
|
+
# directory associated with this session.
|
47
|
+
#
|
48
|
+
# === Return
|
49
|
+
# valid(true|false):: True if the session is valid, false otherwise
|
26
50
|
def valid?
|
27
51
|
@directory.valid_session?(@id, @expired_at)
|
28
52
|
end
|
29
53
|
|
54
|
+
# Serialize the session to a form suitable for use with HTTP cookies. If any
|
55
|
+
# secure attributes have changed since the session was instantiated, compute
|
56
|
+
# a fresh RSA signature.
|
57
|
+
#
|
58
|
+
# === Return
|
59
|
+
# cookie(String):: The B64cookie-encoded Zlib-compressed JSON-serialized global session hash
|
30
60
|
def to_s
|
31
61
|
if @cookie && !@dirty_insecure && !@dirty_secure
|
32
62
|
#use cached cookie if nothing has changed
|
@@ -57,35 +87,84 @@ module HasGlobalSession
|
|
57
87
|
return Encoding::Base64Cookie.dump(zbin)
|
58
88
|
end
|
59
89
|
|
90
|
+
# Determine whether the global session schema allows a given key to be placed
|
91
|
+
# in the global session.
|
92
|
+
#
|
93
|
+
# === Parameters
|
94
|
+
# key(String):: The name of the key
|
95
|
+
#
|
96
|
+
# === Return
|
97
|
+
# supported(true|false):: Whether the specified key is supported
|
60
98
|
def supports_key?(key)
|
61
99
|
@schema_signed.include?(key) || @schema_insecure.include?(key)
|
62
100
|
end
|
63
101
|
|
102
|
+
# Determine whether this session contains a value with the specified key.
|
103
|
+
#
|
104
|
+
# === Parameters
|
105
|
+
# key(String):: The name of the key
|
106
|
+
#
|
107
|
+
# === Return
|
108
|
+
# contained(true|false):: Whether the session currently has a value for the specified key.
|
64
109
|
def has_key?(key)
|
65
110
|
@signed.has_key(key) || @insecure.has_key?(key)
|
66
111
|
end
|
67
112
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
113
|
+
# Return the keys that are currently present in the global session.
|
114
|
+
#
|
115
|
+
# === Return
|
116
|
+
# keys(Array):: List of keys contained in the global session
|
72
117
|
def keys
|
73
118
|
@signed.keys + @insecure.keys
|
74
119
|
end
|
75
120
|
|
121
|
+
# Return the values that are currently present in the global session.
|
122
|
+
#
|
123
|
+
# === Return
|
124
|
+
# values(Array):: List of values contained in the global session
|
76
125
|
def values
|
77
126
|
@signed.values + @insecure.values
|
78
127
|
end
|
79
128
|
|
80
|
-
|
129
|
+
# Iterate over each key/value pair
|
130
|
+
#
|
131
|
+
# === Block
|
132
|
+
# An iterator which will be called with each key/value pair
|
133
|
+
#
|
134
|
+
# === Return
|
135
|
+
# Returns the value of the last expression evaluated by the block
|
136
|
+
def each_pair(&block) # :yields: |key, value|
|
81
137
|
@signed.each_pair(&block)
|
82
138
|
@insecure.each_pair(&block)
|
83
139
|
end
|
84
140
|
|
141
|
+
# Lookup a value by its key.
|
142
|
+
#
|
143
|
+
# === Parameters
|
144
|
+
# key(String):: the key
|
145
|
+
#
|
146
|
+
# === Return
|
147
|
+
# value(Object):: The value associated with +key+, or nil if +key+ is not present
|
85
148
|
def [](key)
|
86
149
|
@signed[key] || @insecure[key]
|
87
150
|
end
|
88
151
|
|
152
|
+
# Set a value in the global session hash. If the supplied key is denoted as
|
153
|
+
# secure by the global session schema, causes a new signature to be computed
|
154
|
+
# when the session is next serialized.
|
155
|
+
#
|
156
|
+
# === Parameters
|
157
|
+
# key(String):: The key to set
|
158
|
+
# value(Object):: The value to set
|
159
|
+
#
|
160
|
+
# === Return
|
161
|
+
# value(Object):: Always returns the value that was set
|
162
|
+
#
|
163
|
+
# ===Raise
|
164
|
+
# InvalidSession:: if the session has been invalidated (and therefore can't be written to)
|
165
|
+
# ArgumentError:: if the configuration doesn't define the specified key as part of the global session
|
166
|
+
# NoAuthority:: if the specified key is secure and the local node is not an authority
|
167
|
+
# UnserializableType:: if the specified value can't be serialized as JSON
|
89
168
|
def []=(key, value)
|
90
169
|
raise InvalidSession unless valid?
|
91
170
|
|
@@ -102,36 +181,58 @@ module HasGlobalSession
|
|
102
181
|
else
|
103
182
|
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
104
183
|
end
|
184
|
+
|
185
|
+
return value
|
105
186
|
end
|
106
187
|
|
188
|
+
# Invalidate this session by reporting its UUID to the Directory.
|
189
|
+
#
|
190
|
+
# === Return
|
191
|
+
# unknown(Object):: Returns whatever the Directory returns
|
107
192
|
def invalidate!
|
108
193
|
@directory.report_invalid_session(@id, @expired_at)
|
109
194
|
end
|
110
195
|
|
196
|
+
# Renews this global session, changing its expiry timestamp into the future.
|
197
|
+
# Causes a new signature will be computed when the session is next serialized.
|
198
|
+
#
|
199
|
+
# === Return
|
200
|
+
# true:: Always returns true
|
111
201
|
def renew!
|
112
202
|
authority_check
|
113
203
|
@expired_at = Configuration['timeout'].to_i.minutes.from_now.utc
|
114
204
|
@dirty_secure = true
|
115
205
|
end
|
116
206
|
|
207
|
+
# Return the SHA1 hash of the most recently-computed RSA signature of this session.
|
208
|
+
# This isn't really intended for the end user; it exists so the Web framework integration
|
209
|
+
# code can optimize request speed by caching the most recently verified signature in the
|
210
|
+
# local session and avoid re-verifying it on every request.
|
211
|
+
#
|
212
|
+
# === Return
|
213
|
+
# digest(String):: SHA1 hex-digest of most-recently-computed signature
|
214
|
+
def signature_digest
|
215
|
+
@signature ? digest(@signature) : nil
|
216
|
+
end
|
217
|
+
|
117
218
|
private
|
118
219
|
|
119
|
-
def authority_check
|
220
|
+
def authority_check # :nodoc:
|
120
221
|
unless @directory.local_authority_name
|
121
222
|
raise NoAuthority, 'Cannot change secure session attributes; we are not an authority'
|
122
223
|
end
|
123
224
|
end
|
124
225
|
|
125
|
-
def canonical_digest(input)
|
226
|
+
def canonical_digest(input) # :nodoc:
|
126
227
|
canonical = Encoding::JSON.dump(canonicalize(input))
|
127
228
|
return digest(canonical)
|
128
229
|
end
|
129
230
|
|
130
|
-
def digest(input)
|
231
|
+
def digest(input) # :nodoc:
|
131
232
|
return Digest::SHA1.new().update(input).hexdigest
|
132
233
|
end
|
133
234
|
|
134
|
-
def canonicalize(input)
|
235
|
+
def canonicalize(input) # :nodoc:
|
135
236
|
case input
|
136
237
|
when Hash
|
137
238
|
output = Array.new
|
@@ -150,10 +251,16 @@ module HasGlobalSession
|
|
150
251
|
return output
|
151
252
|
end
|
152
253
|
|
153
|
-
def load_from_cookie(cookie, valid_signature_digest)
|
154
|
-
|
155
|
-
|
156
|
-
|
254
|
+
def load_from_cookie(cookie, valid_signature_digest) # :nodoc:
|
255
|
+
begin
|
256
|
+
zbin = Encoding::Base64Cookie.load(cookie)
|
257
|
+
json = Zlib::Inflate.inflate(zbin)
|
258
|
+
hash = Encoding::JSON.load(json)
|
259
|
+
rescue Exception => e
|
260
|
+
mc = MalformedCookie.new("Caused by #{e.class.name}: #{e.message}")
|
261
|
+
mc.set_backtrace(e.backtrace)
|
262
|
+
raise mc
|
263
|
+
end
|
157
264
|
|
158
265
|
id = hash['id']
|
159
266
|
authority = hash['a']
|
@@ -200,7 +307,7 @@ module HasGlobalSession
|
|
200
307
|
@cookie = cookie
|
201
308
|
end
|
202
309
|
|
203
|
-
def create_from_scratch
|
310
|
+
def create_from_scratch # :nodoc:
|
204
311
|
authority_check
|
205
312
|
|
206
313
|
@signed = {}
|
@@ -219,7 +326,7 @@ module HasGlobalSession
|
|
219
326
|
renew!
|
220
327
|
end
|
221
328
|
|
222
|
-
def create_invalid
|
329
|
+
def create_invalid # :nodoc:
|
223
330
|
@id = nil
|
224
331
|
@created_at = Time.now.utc
|
225
332
|
@expired_at = created_at
|
@@ -1,12 +1,42 @@
|
|
1
1
|
module HasGlobalSession
|
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
|
+
#
|
2
15
|
class IntegratedSession
|
3
|
-
|
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
|
4
21
|
|
22
|
+
# Construct a new integrated session.
|
23
|
+
#
|
24
|
+
# === Parameters
|
25
|
+
# local(Object):: Local session that acts like a Hash
|
26
|
+
# global(GlobalSession):: GlobalSession
|
5
27
|
def initialize(local, global)
|
6
28
|
@local = local
|
7
29
|
@global = global
|
8
30
|
end
|
9
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
|
10
40
|
def [](key)
|
11
41
|
key = key.to_s
|
12
42
|
if @global.supports_key?(key)
|
@@ -16,6 +46,15 @@ module HasGlobalSession
|
|
16
46
|
end
|
17
47
|
end
|
18
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
|
19
58
|
def []=(key, value)
|
20
59
|
key = key.to_s
|
21
60
|
if @global.supports_key?(key)
|
@@ -23,34 +62,48 @@ module HasGlobalSession
|
|
23
62
|
else
|
24
63
|
@local[key] = value
|
25
64
|
end
|
65
|
+
|
66
|
+
return value
|
26
67
|
end
|
27
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.
|
28
76
|
def has_key?(key)
|
29
77
|
key = key.to_s
|
30
|
-
@global.has_key(key) || @local.has_key?(key)
|
78
|
+
@global.has_key?(key) || @local.has_key?(key)
|
31
79
|
end
|
32
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.
|
33
85
|
def keys
|
34
86
|
@global.keys + @local.keys
|
35
87
|
end
|
36
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.
|
37
93
|
def values
|
38
94
|
@global.values + @local.values
|
39
95
|
end
|
40
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
|
41
104
|
def each_pair(&block)
|
42
105
|
@global.each_pair(&block)
|
43
106
|
@local.each_pair(&block)
|
44
107
|
end
|
45
|
-
|
46
|
-
def method_missing(meth, *args)
|
47
|
-
if @global.respond_to?(meth)
|
48
|
-
return @global.send(meth, *args)
|
49
|
-
elsif @local.respond_to?(meth)
|
50
|
-
return @local.send(meth, *args)
|
51
|
-
else
|
52
|
-
super
|
53
|
-
end
|
54
|
-
end
|
55
108
|
end
|
56
109
|
end
|
@@ -1,17 +1,39 @@
|
|
1
1
|
module HasGlobalSession
|
2
|
+
# Rails integration for HasGlobalSession.
|
3
|
+
#
|
4
|
+
# The configuration file for Rails apps is located in +config/global_session.yml+ and a generator
|
5
|
+
# (global_session_config) is available for creating a sensible default.
|
6
|
+
#
|
7
|
+
# There is also a generator (global_session_authority) for creating authority keypairs.
|
8
|
+
#
|
9
|
+
# The main integration touchpoint for Rails is the module ActionControllerInstanceMethods,
|
10
|
+
# which gets mixed into ActionController::Base. This is where all of the magic happens..
|
11
|
+
#
|
2
12
|
module Rails
|
13
|
+
# Module that is mixed into ActionController-derived classes when the class method
|
14
|
+
# +has_global_session+ is called.
|
15
|
+
#
|
3
16
|
module ActionControllerInstanceMethods
|
4
|
-
def self.included(base)
|
17
|
+
def self.included(base) # :nodoc:
|
5
18
|
base.alias_method_chain :session, :global_session
|
6
19
|
base.before_filter :global_session_read_cookie
|
7
20
|
base.before_filter :global_session_auto_renew
|
8
21
|
base.after_filter :global_session_update_cookie
|
9
22
|
end
|
10
23
|
|
24
|
+
# Global session reader.
|
25
|
+
#
|
26
|
+
# === Return
|
27
|
+
# session(GlobalSession):: the global session associated with the current request, nil if none
|
11
28
|
def global_session
|
12
29
|
@global_session
|
13
30
|
end
|
14
31
|
|
32
|
+
# Aliased version of ActionController::Base#session which will return the integrated
|
33
|
+
# global-and-local session object (IntegratedSession).
|
34
|
+
#
|
35
|
+
# === Return
|
36
|
+
# session(IntegratedSession):: the integrated session
|
15
37
|
def session_with_global_session
|
16
38
|
if Configuration['integrated'] && @global_session
|
17
39
|
unless @integrated_session &&
|
@@ -27,6 +49,11 @@ module HasGlobalSession
|
|
27
49
|
end
|
28
50
|
end
|
29
51
|
|
52
|
+
# Before-filter to read the global session cookie and construct the GlobalSession object
|
53
|
+
# for this controller instance.
|
54
|
+
#
|
55
|
+
# === Return
|
56
|
+
# true:: Always returns true
|
30
57
|
def global_session_read_cookie
|
31
58
|
directory = global_session_create_directory
|
32
59
|
cookie_name = Configuration['cookie']['name']
|
@@ -56,15 +83,27 @@ module HasGlobalSession
|
|
56
83
|
end
|
57
84
|
end
|
58
85
|
|
86
|
+
# Before-filter to renew the global session if it will be expiring soon.
|
87
|
+
#
|
88
|
+
# === Return
|
89
|
+
# true:: Always returns true
|
59
90
|
def global_session_auto_renew
|
60
91
|
#Auto-renew session if needed
|
61
92
|
renew = Configuration['renew']
|
62
|
-
if @global_session
|
93
|
+
if @global_session &&
|
94
|
+
renew &&
|
95
|
+
@global_session.directory.local_authority_name &&
|
63
96
|
@global_session.expired_at < renew.to_i.minutes.from_now.utc
|
64
97
|
@global_session.renew!
|
65
|
-
end
|
98
|
+
end
|
99
|
+
|
100
|
+
return true
|
66
101
|
end
|
67
102
|
|
103
|
+
# After-filter to write any pending changes to the global session cookie.
|
104
|
+
#
|
105
|
+
# === Return
|
106
|
+
# true:: Always returns true
|
68
107
|
def global_session_update_cookie
|
69
108
|
name = Configuration['cookie']['name']
|
70
109
|
domain = Configuration['cookie']['domain'] || request.env['SERVER_NAME']
|
@@ -90,6 +129,15 @@ module HasGlobalSession
|
|
90
129
|
end
|
91
130
|
end
|
92
131
|
|
132
|
+
# Override for the ActionController method of the same name that logs
|
133
|
+
# information about the request. Our version logs the global session ID
|
134
|
+
# instead of the local session ID.
|
135
|
+
#
|
136
|
+
# === Parameters
|
137
|
+
# name(Type):: Description
|
138
|
+
#
|
139
|
+
# === Return
|
140
|
+
# name(Type):: Description
|
93
141
|
def log_processing
|
94
142
|
if logger && logger.info?
|
95
143
|
log_processing_for_request_id
|
@@ -97,7 +145,7 @@ module HasGlobalSession
|
|
97
145
|
end
|
98
146
|
end
|
99
147
|
|
100
|
-
def log_processing_for_request_id
|
148
|
+
def log_processing_for_request_id # :nodoc:
|
101
149
|
if global_session && global_session.id
|
102
150
|
session_id = global_session.id + " (#{session[:session_id]})"
|
103
151
|
elsif session[:session_id]
|
@@ -114,7 +162,7 @@ module HasGlobalSession
|
|
114
162
|
logger.info(request_id)
|
115
163
|
end
|
116
164
|
|
117
|
-
def log_processing_for_parameters
|
165
|
+
def log_processing_for_parameters # :nodoc:
|
118
166
|
parameters = respond_to?(:filter_parameters) ? filter_parameters(params) : params.dup
|
119
167
|
parameters = parameters.except!(:controller, :action, :format, :_method)
|
120
168
|
|
@@ -123,7 +171,7 @@ module HasGlobalSession
|
|
123
171
|
|
124
172
|
private
|
125
173
|
|
126
|
-
def global_session_create_directory
|
174
|
+
def global_session_create_directory # :nodoc:
|
127
175
|
if (klass = Configuration['directory'])
|
128
176
|
klass = klass.constantize
|
129
177
|
else
|
data/lib/has_global_session.rb
CHANGED
@@ -1,9 +1,46 @@
|
|
1
1
|
module HasGlobalSession
|
2
|
+
# Indicates that the global session configuration file is missing from disk.
|
3
|
+
#
|
2
4
|
class MissingConfiguration < Exception; end
|
5
|
+
|
6
|
+
# Indicates that the global session configuration file is missing elements or is
|
7
|
+
# malformatted.
|
8
|
+
#
|
3
9
|
class ConfigurationError < Exception; end
|
10
|
+
|
11
|
+
# Indicates that a client submitted a request with a valid session cookie, but the
|
12
|
+
# session ID was reported as invalid by the Directory.
|
13
|
+
#
|
14
|
+
# See Directory#valid_session? for more information.
|
15
|
+
#
|
4
16
|
class InvalidSession < Exception; end
|
17
|
+
|
18
|
+
# Indicates that a client submitted a request with a valid session cookie, but the
|
19
|
+
# session has expired.
|
20
|
+
#
|
5
21
|
class ExpiredSession < Exception; end
|
22
|
+
|
23
|
+
# Indicates that a client submitted a request with a session cookie that could not
|
24
|
+
# be decoded or decompressed.
|
25
|
+
#
|
26
|
+
class MalformedCookie < Exception; end
|
27
|
+
|
28
|
+
# Indicates that application code tried to put an unserializable object into the glboal
|
29
|
+
# session hash. Because the global session is serialized as JSON and not all Ruby types
|
30
|
+
# can be easily round-tripped to JSON and back without data loss, we constrain the types
|
31
|
+
# that can be serialized.
|
32
|
+
#
|
33
|
+
# See HasGlobalSession::Encoding::JSON for more information on serializable types.
|
34
|
+
#
|
6
35
|
class UnserializableType < Exception; end
|
36
|
+
|
37
|
+
# Indicates that the application code tried to write a secure session attribute or
|
38
|
+
# renew the global session. Both of these operations require a local authority
|
39
|
+
# because they require a new signature to be computed on the global session.
|
40
|
+
#
|
41
|
+
# See HasGlobalSession::Configuration and HasGlobalSession::Directory for more
|
42
|
+
# information.
|
43
|
+
#
|
7
44
|
class NoAuthority < Exception; end
|
8
45
|
end
|
9
46
|
|
metadata
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: has_global_session
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 15
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
|
+
- 1
|
7
8
|
- 0
|
8
|
-
|
9
|
-
- 5
|
10
|
-
version: 0.9.5
|
9
|
+
version: "1.0"
|
11
10
|
platform: ruby
|
12
11
|
authors:
|
13
12
|
- Tony Spataro
|
@@ -15,7 +14,7 @@ autorequire:
|
|
15
14
|
bindir: bin
|
16
15
|
cert_chain: []
|
17
16
|
|
18
|
-
date: 2010-07-
|
17
|
+
date: 2010-07-27 00:00:00 -07:00
|
19
18
|
default_executable:
|
20
19
|
dependencies:
|
21
20
|
- !ruby/object:Gem::Dependency
|