has_global_session 0.9.5 → 1.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/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
|