global_session 3.0.5 → 3.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.
- checksums.yaml +7 -0
- data/.travis.yml +8 -0
- data/CHANGELOG.rdoc +23 -6
- data/LICENSE +1 -1
- data/README.rdoc +84 -38
- data/Rakefile +19 -17
- data/VERSION +1 -1
- data/global_session.gemspec +18 -34
- data/lib/global_session.rb +1 -0
- data/lib/global_session/configuration.rb +59 -12
- data/lib/global_session/directory.rb +71 -43
- data/lib/global_session/keystore.rb +139 -0
- data/lib/global_session/rack.rb +98 -76
- data/lib/global_session/rails.rb +4 -0
- metadata +121 -236
@@ -19,6 +19,8 @@
|
|
19
19
|
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
20
|
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
21
|
|
22
|
+
require 'yaml'
|
23
|
+
|
22
24
|
module GlobalSession
|
23
25
|
# Central point of access for GlobalSession configuration information. This is
|
24
26
|
# mostly a very thin wrapper around the serialized hash written to the YAML config
|
@@ -35,8 +37,8 @@ module GlobalSession
|
|
35
37
|
# * ephemeral
|
36
38
|
# * timeout
|
37
39
|
# * renew
|
38
|
-
# * authority
|
39
|
-
# * trust
|
40
|
+
# * authority (optional - inferred from presence of private key file)
|
41
|
+
# * trust (optional - inferred from presence/name of public key files)
|
40
42
|
# * directory
|
41
43
|
# * cookie
|
42
44
|
# * version
|
@@ -94,7 +96,7 @@ module GlobalSession
|
|
94
96
|
def initialize(config, environment)
|
95
97
|
if config.is_a?(Hash)
|
96
98
|
@config = config
|
97
|
-
elsif File.
|
99
|
+
elsif File.file?(config)
|
98
100
|
data = YAML.load(File.read(config))
|
99
101
|
unless data.is_a?(Hash)
|
100
102
|
raise TypeError, "Configuration file #{File.basename(config)} must contain a hash as its top-level element"
|
@@ -121,9 +123,32 @@ module GlobalSession
|
|
121
123
|
get(key, true)
|
122
124
|
end
|
123
125
|
|
126
|
+
# Writer for configuration elements. Writes to an environment-specific stanza if one is present,
|
127
|
+
# else writes to the common stanza. DOES NOT OVERWRITE the key's value if it already has one!
|
128
|
+
#
|
129
|
+
# @param [String] key
|
130
|
+
# @param optional [Object] the value to write, or empty-hash as a default
|
131
|
+
def []=(key, value={})
|
132
|
+
if @config.has_key?(@environment)
|
133
|
+
@config[@environment][key] ||= value
|
134
|
+
else
|
135
|
+
@config['common'][key] ||= value
|
136
|
+
end
|
137
|
+
rescue NoMethodError
|
138
|
+
raise MissingConfiguration, "Configuration key '#{key}' not found"
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determine whether a given configuration key was specified.
|
142
|
+
#
|
143
|
+
# @return [Boolean] true if the key is present in the common or per-environment stanzas
|
144
|
+
def has_key?(k)
|
145
|
+
@config[@environment].has_key?(k) || @config['common'].has_key?(k)
|
146
|
+
end
|
147
|
+
|
148
|
+
alias key? has_key?
|
149
|
+
|
124
150
|
def validate # :nodoc
|
125
|
-
['attributes/signed', 'cookie/name',
|
126
|
-
'timeout'].each {|k| validate_presence_of k}
|
151
|
+
['attributes/signed', 'cookie/name', 'timeout'].each {|k| validate_presence_of k}
|
127
152
|
end
|
128
153
|
|
129
154
|
protected
|
@@ -137,7 +162,12 @@ module GlobalSession
|
|
137
162
|
# true always
|
138
163
|
def validate_presence_of(key)
|
139
164
|
elements = key.split '/'
|
140
|
-
|
165
|
+
top_key = elements.shift
|
166
|
+
object = get(top_key, true) # pretend we're validated in order to get inheritance
|
167
|
+
if object.nil?
|
168
|
+
msg = "Configuration does not specify required element '#{top_key}'"
|
169
|
+
raise MissingConfiguration, msg
|
170
|
+
end
|
141
171
|
elements.each do |element|
|
142
172
|
object = object[element] if object
|
143
173
|
if object.nil?
|
@@ -150,15 +180,32 @@ module GlobalSession
|
|
150
180
|
|
151
181
|
private
|
152
182
|
|
183
|
+
# Get a configuration key.
|
184
|
+
#
|
185
|
+
# @return [Object] the value of the desired key
|
186
|
+
# @raise [MissingConfiguration] if the key is not found
|
187
|
+
# @param [String] key
|
188
|
+
# @param [Boolean] if true, check both the common and per-environment stanzas for the key
|
153
189
|
def get(key, validated) # :nodoc
|
154
|
-
if
|
155
|
-
|
156
|
-
|
190
|
+
if validated
|
191
|
+
# Fancy inheritance logic
|
192
|
+
if ('common' == key) && @config.key?(key)
|
193
|
+
# The common stanza itself
|
194
|
+
@config[key]
|
195
|
+
elsif (@environment == key) && @config.key?(key)
|
196
|
+
# The environment-specific stanza itself
|
197
|
+
@config[key]
|
198
|
+
elsif @config.key?(@environment) && @config[@environment].key?(key)
|
199
|
+
# Some key in the environment-specific stanza
|
200
|
+
return @config[@environment][key]
|
201
|
+
elsif @config.key?('common') && @config['common'].key?(key)
|
202
|
+
# By process of elimination, some key in the common stanza
|
203
|
+
@config['common'][key]
|
204
|
+
end
|
157
205
|
else
|
158
|
-
|
206
|
+
# Fail sauce
|
207
|
+
raise MissingConfiguration, "Configuration key '#{key}' not found"
|
159
208
|
end
|
160
|
-
rescue NoMethodError
|
161
|
-
raise MissingConfiguration, "Configuration key '#{key}' not found"
|
162
209
|
end
|
163
210
|
end
|
164
211
|
end
|
@@ -22,7 +22,7 @@
|
|
22
22
|
require 'set'
|
23
23
|
|
24
24
|
module GlobalSession
|
25
|
-
# The global session directory, which provides
|
25
|
+
# The global session directory, which provides lookup and decision services
|
26
26
|
# to instances of Session.
|
27
27
|
#
|
28
28
|
# The default implementation is simplistic, but should be suitable for most applications.
|
@@ -31,28 +31,23 @@ module GlobalSession
|
|
31
31
|
# setting to specify the class name of your implementation:
|
32
32
|
#
|
33
33
|
# common:
|
34
|
-
# directory:
|
34
|
+
# directory:
|
35
|
+
# class: MyCoolDirectory
|
35
36
|
#
|
37
|
+
# == Key Management
|
36
38
|
#
|
37
|
-
#
|
38
|
-
# Directory
|
39
|
-
#
|
40
|
-
# contain one or more +*.pub+ files containing OpenSSH-format public
|
41
|
-
# RSA keys. The name of the pub file determines the name of the
|
42
|
-
# authority it represents.
|
39
|
+
# All key-related functionality has been delegated to the Keystore class as of
|
40
|
+
# v3.1. Directory retains its key management hooks for downrev compatibility,
|
41
|
+
# but mostly they are stubs for Keystore functionality.
|
43
42
|
#
|
44
|
-
#
|
45
|
-
# Directory will infer the name of the local authority (if any) by
|
46
|
-
# looking for a private-key file in the keystore. If a +*.key+ file
|
47
|
-
# is found, then its name is taken to be the name of the local
|
48
|
-
# authority and all GlobalSessions created will be signed by that
|
49
|
-
# authority's private key.
|
50
|
-
#
|
51
|
-
# If more than one key file is found, Directory will raise an error
|
52
|
-
# at initialization time.
|
43
|
+
# For more information about key mangement, please refer to the Keystore class.
|
53
44
|
#
|
54
45
|
class Directory
|
55
|
-
|
46
|
+
# @return [Configuration] shared configuration object
|
47
|
+
attr_reader :configuration
|
48
|
+
|
49
|
+
# @return [Keystore] asymmetric crypto keys for signing authorities
|
50
|
+
attr_reader :keystore
|
56
51
|
|
57
52
|
# @return a representation of the object suitable for printing to the console
|
58
53
|
def inspect
|
@@ -61,31 +56,36 @@ module GlobalSession
|
|
61
56
|
|
62
57
|
# Create a new Directory.
|
63
58
|
#
|
64
|
-
#
|
65
|
-
# keystore_directory(
|
66
|
-
#
|
67
|
-
|
68
|
-
# ConfigurationError:: if too many or too few keys are found, or if *.key/*.pub files are malformatted
|
69
|
-
def initialize(configuration, keystore_directory)
|
59
|
+
# @param [Configuration] shared configuration
|
60
|
+
# @param optional [String] keystore_directory (DEPRECATED) if present, directory where keys can be found
|
61
|
+
# @raise [ConfigurationError] if too many or too few keys are found, or if *.key/*.pub files are malformatted
|
62
|
+
def initialize(configuration, keystore_directory=nil)
|
70
63
|
@configuration = configuration
|
71
|
-
certs = Dir[File.join(keystore_directory, '*.pub')]
|
72
|
-
keys = Dir[File.join(keystore_directory, '*.key')]
|
73
|
-
|
74
64
|
@authorities = {}
|
75
|
-
certs.each do |cert_file|
|
76
|
-
basename = File.basename(cert_file)
|
77
|
-
authority = basename[0...(basename.rindex('.'))] #chop trailing .ext
|
78
|
-
@authorities[authority] = OpenSSL::PKey::RSA.new(File.read(cert_file))
|
79
|
-
raise ConfigurationError, "Expected #{basename} to contain an RSA public key" unless @authorities[authority].public?
|
80
|
-
end
|
81
65
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
66
|
+
# Propagate a deprecated parameter
|
67
|
+
# @deprecated remove for v4.0
|
68
|
+
if keystore_directory.is_a?(String)
|
69
|
+
all_files = Dir.glob(File.join(keystore_directory, '*'))
|
70
|
+
public_keys = all_files.select { |kf| kf =~ /\.pub$/ }
|
71
|
+
raise ConfigurationError, "No public keys (*.pub) found in #{keystore_directory}" if public_keys.empty?
|
72
|
+
|
73
|
+
@configuration['common'] ||= {}
|
74
|
+
@configuration['common']['keystore'] ||= {}
|
75
|
+
@configuration['common']['keystore']['public'] = [keystore_directory]
|
76
|
+
|
77
|
+
# Propagate a deprecated configuration option
|
78
|
+
# @deprecated remove for v4.0
|
79
|
+
if (private_key = @configuration['authority'])
|
80
|
+
key_file = all_files.detect { |kf| kf =~ /#{private_key}\.key$/ }
|
81
|
+
raise ConfigurationError, "Key file #{private_key}.key not found in #{keystore_directory}" unless key_file
|
82
|
+
@configuration['common'] ||= {}
|
83
|
+
@configuration['common']['keystore'] ||= {}
|
84
|
+
@configuration['common']['keystore']['private'] = key_file
|
85
|
+
end
|
87
86
|
end
|
88
87
|
|
88
|
+
@keystore = Keystore.new(configuration)
|
89
89
|
@invalid_sessions = Set.new
|
90
90
|
end
|
91
91
|
|
@@ -95,6 +95,7 @@ module GlobalSession
|
|
95
95
|
# DEPRECATED: If a cookie is provided, load an existing session from its
|
96
96
|
# serialized form. You should use #load_session for this instead.
|
97
97
|
#
|
98
|
+
# @deprecated will be removed in GlobalSession v4; please use #load_session instead
|
98
99
|
# @see load_session
|
99
100
|
#
|
100
101
|
# === Parameters
|
@@ -146,12 +147,33 @@ module GlobalSession
|
|
146
147
|
Session.new(self, cookie)
|
147
148
|
end
|
148
149
|
|
150
|
+
# @return [Hash] map of String authority-names to OpenSSL::PKey public-keys
|
151
|
+
# @deprecated will be removed in GlobalSession v4; please use Keystore instead
|
152
|
+
# @see GlobalSession::Keystore
|
153
|
+
def authorities
|
154
|
+
@keystore.public_keys
|
155
|
+
end
|
156
|
+
|
157
|
+
# Determine the private key associated with this directory, to be used for signing.
|
158
|
+
#
|
159
|
+
# @return [nil,OpenSSL::PKey] local authority key if we are an authority, else nil
|
160
|
+
# @deprecated will be removed in GlobalSession v4; please use Keystore instead
|
161
|
+
# @see GlobalSession::Keystore
|
162
|
+
def private_key
|
163
|
+
@keystore.private_key || @private_key
|
164
|
+
end
|
165
|
+
|
166
|
+
# Determine the authority name associated with this directory's private session-signing key.
|
167
|
+
#
|
168
|
+
# @deprecated will be removed in GlobalSession v4; please use Keystore instead
|
169
|
+
# @see GlobalSession::Keystore
|
149
170
|
def local_authority_name
|
150
|
-
@
|
171
|
+
@keystore.private_key_name || @private_key_name
|
151
172
|
end
|
152
173
|
|
153
|
-
# Determine whether this system trusts a particular authority based on
|
154
|
-
# the
|
174
|
+
# Determine whether this system trusts a particular named authority based on
|
175
|
+
# the settings specified in Configuration and/or the presence of public key
|
176
|
+
# files on disk.
|
155
177
|
#
|
156
178
|
# === Parameters
|
157
179
|
# authority(String):: The name of the authority
|
@@ -159,7 +181,13 @@ module GlobalSession
|
|
159
181
|
# === Return
|
160
182
|
# trusted(true|false):: whether the local system trusts sessions signed by the specified authority
|
161
183
|
def trusted_authority?(authority)
|
162
|
-
@configuration
|
184
|
+
if @configuration.has_key?('trust')
|
185
|
+
# Explicit trust in just the authorities specified in the configuration
|
186
|
+
@configuration['trust'].include?(authority)
|
187
|
+
else
|
188
|
+
# Implicit trust in any public key we found on disk
|
189
|
+
@keystore.public_keys.keys.include?(authority)
|
190
|
+
end
|
163
191
|
end
|
164
192
|
|
165
193
|
# Determine whether the given session UUID is valid. The default implementation only considers
|
@@ -190,5 +218,5 @@ module GlobalSession
|
|
190
218
|
def report_invalid_session(uuid, expired_at)
|
191
219
|
@invalid_sessions << uuid
|
192
220
|
end
|
193
|
-
end
|
221
|
+
end
|
194
222
|
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# Copyright (c) 2014- RightScale Inc
|
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.
|
21
|
+
|
22
|
+
require 'set'
|
23
|
+
require 'uri'
|
24
|
+
|
25
|
+
module GlobalSession
|
26
|
+
# Keystore uses one or more filesystem directories as a backing store
|
27
|
+
# for RSA keys of global session authorities. The directories should
|
28
|
+
# contain one or more +*.pub+ files containing OpenSSH-format public
|
29
|
+
# RSA keys. The name of the pub file determines the name of the
|
30
|
+
# authority it represents.
|
31
|
+
#
|
32
|
+
# === The Local Authority
|
33
|
+
# Directory will infer the name of the local authority (if any) by
|
34
|
+
# looking for a private-key file in the keystore. If a +*.key+ file
|
35
|
+
# is found, then its name is taken to be the name of the local
|
36
|
+
# authority and all GlobalSessions created will be signed by that
|
37
|
+
# authority's private key.
|
38
|
+
#
|
39
|
+
# If more than one private key file is found, Directory will raise
|
40
|
+
# an error at initialization time.
|
41
|
+
#
|
42
|
+
class Keystore
|
43
|
+
# @return [Configuration] shared configuration object
|
44
|
+
attr_reader :configuration
|
45
|
+
|
46
|
+
# @return [Hash] map of String authority-names to OpenSSL::PKey public-keys
|
47
|
+
attr_reader :public_keys
|
48
|
+
|
49
|
+
# @return [nil, String] name of local authority if we are one, else nil
|
50
|
+
attr_reader :private_key_name
|
51
|
+
|
52
|
+
# @return [nil,OpenSSL::PKey] local authority key if we are an authority, else nil
|
53
|
+
attr_reader :private_key
|
54
|
+
|
55
|
+
# @return a representation of the object suitable for printing to the console
|
56
|
+
def inspect
|
57
|
+
"<#{self.class.name} @configuration=#{@configuration.inspect}>"
|
58
|
+
end
|
59
|
+
|
60
|
+
def initialize(configuration)
|
61
|
+
@configuration = configuration
|
62
|
+
load
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
# Load all public and/or private keys from location(s) specified in the configuration's
|
68
|
+
# "keystore/public" and "keystore/private" directives.
|
69
|
+
#
|
70
|
+
# @raise [ConfigurationError] if some authority's public key has already been loaded
|
71
|
+
def load
|
72
|
+
locations = Array((configuration['keystore'] || {})['public'] || [])
|
73
|
+
|
74
|
+
locations.each do |location|
|
75
|
+
load_public_key(location)
|
76
|
+
end
|
77
|
+
|
78
|
+
location = (configuration['keystore'] || {})['private']
|
79
|
+
location ||= ENV['GLOBAL_SESSION_PRIVATE_KEY']
|
80
|
+
load_private_key(location) if location # then we must be an authority; load our key
|
81
|
+
end
|
82
|
+
|
83
|
+
# Load a single authority's public key, or an entire directory full of public keys. Assume
|
84
|
+
# that the basenames of the key files are the authority names, e.g. "dev.pub" --> "dev".
|
85
|
+
#
|
86
|
+
# @param [String] path to file or directory to load
|
87
|
+
# @raise [Errno::ENOENT] if path is neither a file nor a directory
|
88
|
+
# @raise [ConfigurationError] if some authority's public key has already been loaded
|
89
|
+
def load_public_key(path)
|
90
|
+
@public_keys ||= {}
|
91
|
+
|
92
|
+
if File.directory?(path)
|
93
|
+
Dir.glob(File.join(path, '*')).each do |file|
|
94
|
+
load_public_key(file)
|
95
|
+
end
|
96
|
+
elsif File.file?(path)
|
97
|
+
name = File.basename(path, '.*')
|
98
|
+
key = OpenSSL::PKey::RSA.new(File.read(path))
|
99
|
+
# ignore private keys (which legacy config allowed to coexist with public keys)
|
100
|
+
unless key.private?
|
101
|
+
if @public_keys.has_key?(name)
|
102
|
+
raise ConfigurationError, "Duplicate public key for authority: #{name}"
|
103
|
+
else
|
104
|
+
@public_keys[name] = key
|
105
|
+
end
|
106
|
+
end
|
107
|
+
else
|
108
|
+
raise Errno::ENOENT.new("Path is neither a file nor a directory: " + path)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
# Load a private key. Assume that the basename of the key file is the local authority name,
|
113
|
+
# e.g. "dev.key" --> "dev".
|
114
|
+
#
|
115
|
+
# @param [String] path to private-key file
|
116
|
+
# @raise [Errno::ENOENT] if path is not a file
|
117
|
+
# @raise [ConfigurationError] if some private key has already been loaded
|
118
|
+
def load_private_key(path)
|
119
|
+
if File.directory?(path)
|
120
|
+
# Arbitrarily pick the first key file found in the directory
|
121
|
+
path = Dir.glob(File.join(path, '*.key')).first
|
122
|
+
end
|
123
|
+
|
124
|
+
if File.file?(path)
|
125
|
+
if @private_key.nil?
|
126
|
+
name = File.basename(path, '.*')
|
127
|
+
private_key = OpenSSL::PKey::RSA.new(File.read(path))
|
128
|
+
raise ConfigurationError, "Expected #{key_file} to contain an RSA private key" unless private_key.private?
|
129
|
+
@private_key = private_key
|
130
|
+
@private_key_name = name
|
131
|
+
else
|
132
|
+
raise ConfigurationError, "Only one private key is allowed; already loaded #{@private_key_name}, cannot also load #{path}"
|
133
|
+
end
|
134
|
+
else
|
135
|
+
raise Errno::ENOENT.new("Path is not a file: " + path)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
data/lib/global_session/rack.rb
CHANGED
@@ -29,61 +29,84 @@ module GlobalSession
|
|
29
29
|
class Middleware
|
30
30
|
LOCAL_SESSION_KEY = "rack.session".freeze
|
31
31
|
|
32
|
-
#
|
32
|
+
# @return [GlobalSession::Configuration]
|
33
|
+
attr_accessor :configuration
|
34
|
+
|
35
|
+
# @return [GlobalSession::Directory]
|
36
|
+
attr_accessor :directory
|
37
|
+
|
38
|
+
# Make a new global session middleware.
|
33
39
|
#
|
34
40
|
# The optional block here controls an alternate ticket retrieval
|
35
41
|
# method. If no ticket is stored in the cookie jar, this
|
36
42
|
# function is called. If it returns a non-nil value, that value
|
37
43
|
# is the ticket.
|
38
44
|
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
# configuration(String or Configuration): global_session configuration.
|
42
|
-
# If a string, is interpreted as a
|
43
|
-
# filename to load the config from.
|
44
|
-
# directory(String or Directory): Directory object that provides
|
45
|
-
# trust services to the global
|
46
|
-
# session implementation. If a
|
47
|
-
# string, is interpreted as a
|
48
|
-
# filesystem directory containing
|
49
|
-
# the public and private keys of
|
50
|
-
# authorities, from which default
|
51
|
-
# trust services will be initialized.
|
45
|
+
# @param [Configuration] configuration
|
46
|
+
# @param optional [String,Directory] directory the directory class name (DEPRECATED) or an actual instance of Directory
|
52
47
|
#
|
53
|
-
# block
|
54
|
-
|
48
|
+
# @yield if a block is provided, yields to the block to fetch session data from request state
|
49
|
+
# @yieldparam [Hash] env Rack request environment is passed as a yield parameter
|
50
|
+
def initialize(app, configuration, directory=nil, &block)
|
55
51
|
@app = app
|
56
52
|
|
53
|
+
# Initialize shared configuration
|
54
|
+
# @deprecated require Configuration object in v4
|
57
55
|
if configuration.instance_of?(String)
|
58
56
|
@configuration = Configuration.new(configuration, ENV['RACK_ENV'] || 'development')
|
59
57
|
else
|
60
58
|
@configuration = configuration
|
61
59
|
end
|
62
60
|
|
61
|
+
klass = nil
|
63
62
|
begin
|
64
|
-
|
63
|
+
# v0.9.0 - v3.0.4: class name is the value of the 'directory' key
|
64
|
+
klass_name = @configuration['directory']
|
65
|
+
|
66
|
+
case klass_name
|
67
|
+
when Hash
|
68
|
+
# v3.0.5 and beyond: class name is in 'class' subkey
|
69
|
+
klass_name = klass_name['class']
|
70
|
+
when NilClass
|
71
|
+
# the eternal default, if the class name is not provided
|
72
|
+
klass_name = 'GlobalSession::Directory'
|
73
|
+
end
|
65
74
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
if klass_name.is_a?(String)
|
76
|
+
# for apps
|
77
|
+
klass = klass_name.to_const
|
78
|
+
else
|
79
|
+
# for specs that need to directly inject a class/object
|
80
|
+
klass = klass_name
|
81
|
+
end
|
71
82
|
rescue Exception => e
|
72
|
-
raise GlobalSession::ConfigurationError,
|
83
|
+
raise GlobalSession::ConfigurationError,
|
84
|
+
"Invalid/unknown directory class name: #{klass_name.inspect}"
|
73
85
|
end
|
74
86
|
|
75
|
-
|
76
|
-
|
77
|
-
|
87
|
+
# Initialize the directory
|
88
|
+
# @deprecated require Directory object in v4
|
89
|
+
if klass.is_a?(Class)
|
90
|
+
@directory = klass.new(@configuration, directory)
|
91
|
+
elsif klass.is_a?(Directory)
|
78
92
|
@directory = directory
|
93
|
+
else
|
94
|
+
raise GlobalSession::ConfigurationError,
|
95
|
+
"Unsupported value for 'directory': expected Class or Directory, got #{klass.inspect}"
|
79
96
|
end
|
80
97
|
|
98
|
+
# Initialize the keystore
|
99
|
+
@keystore = Keystore.new(@configuration)
|
100
|
+
|
81
101
|
@cookie_retrieval = block
|
82
|
-
@cookie_name
|
102
|
+
@cookie_name = @configuration['cookie']['name']
|
83
103
|
end
|
84
104
|
|
85
105
|
# Rack request chain. Sets up the global session ticket from
|
86
106
|
# the environment and passes it up the chain.
|
107
|
+
#
|
108
|
+
# @return [Array] valid Rack response tuple e.g. [200, 'hello world']
|
109
|
+
# @param [Hash] env Rack request environment
|
87
110
|
def call(env)
|
88
111
|
env['rack.cookies'] = {} unless env['rack.cookies']
|
89
112
|
|
@@ -121,11 +144,8 @@ module GlobalSession
|
|
121
144
|
# header was found, also disable global session cookie update and renewal by setting the
|
122
145
|
# corresponding keys of the Rack environment.
|
123
146
|
#
|
124
|
-
#
|
125
|
-
#
|
126
|
-
#
|
127
|
-
# === Return
|
128
|
-
# result(true,false):: Returns true if the environment was populated, false otherwise
|
147
|
+
# @return [Boolean] true if the environment was populated, false otherwise
|
148
|
+
# @param [Hash] env Rack request environment
|
129
149
|
def read_authorization_header(env)
|
130
150
|
if env.has_key? 'X-HTTP_AUTHORIZATION'
|
131
151
|
# RFC2617 style (preferred by OAuth 2.0 spec)
|
@@ -138,9 +158,9 @@ module GlobalSession
|
|
138
158
|
end
|
139
159
|
|
140
160
|
if header_data && header_data.size == 2 && header_data.first.downcase == 'bearer'
|
141
|
-
env['global_session.req.renew']
|
161
|
+
env['global_session.req.renew'] = false
|
142
162
|
env['global_session.req.update'] = false
|
143
|
-
env['global_session']
|
163
|
+
env['global_session'] = @directory.load_session(header_data.last)
|
144
164
|
true
|
145
165
|
else
|
146
166
|
false
|
@@ -149,11 +169,8 @@ module GlobalSession
|
|
149
169
|
|
150
170
|
# Read a global session from HTTP cookies, if present.
|
151
171
|
#
|
152
|
-
#
|
153
|
-
#
|
154
|
-
#
|
155
|
-
# === Return
|
156
|
-
# result(true,false):: Returns true if the environment was populated, false otherwise
|
172
|
+
# @return [Boolean] true if the environment was populated, false otherwise
|
173
|
+
# @param [Hash] env Rack request environment
|
157
174
|
def read_cookie(env)
|
158
175
|
if @cookie_retrieval && (cookie = @cookie_retrieval.call(env))
|
159
176
|
env['global_session'] = @directory.load_session(cookie)
|
@@ -169,11 +186,8 @@ module GlobalSession
|
|
169
186
|
# Ensure that the Rack environment contains a global session object; create a session
|
170
187
|
# if necessary.
|
171
188
|
#
|
172
|
-
#
|
173
|
-
#
|
174
|
-
#
|
175
|
-
# === Return
|
176
|
-
# true:: always returns true
|
189
|
+
# @return [true] always returns true
|
190
|
+
# @param [Hash] env Rack request environment
|
177
191
|
def create_session(env)
|
178
192
|
env['global_session'] ||= @directory.create_session
|
179
193
|
|
@@ -182,49 +196,53 @@ module GlobalSession
|
|
182
196
|
|
183
197
|
# Renew the session ticket.
|
184
198
|
#
|
185
|
-
#
|
186
|
-
#
|
199
|
+
# @return [true] always returns true
|
200
|
+
# @param [Hash] env Rack request environment
|
187
201
|
def renew_cookie(env)
|
188
|
-
return unless @
|
202
|
+
return unless @configuration['authority']
|
189
203
|
return if env['global_session.req.renew'] == false
|
190
204
|
|
191
205
|
if (renew = @configuration['renew']) && env['global_session'] &&
|
192
|
-
|
206
|
+
env['global_session'].expired_at < Time.at(Time.now.utc + 60 * renew.to_i)
|
193
207
|
env['global_session'].renew!
|
194
208
|
end
|
209
|
+
|
210
|
+
true
|
195
211
|
end
|
196
212
|
|
197
213
|
# Update the cookie jar with the revised ticket.
|
198
214
|
#
|
199
|
-
#
|
200
|
-
#
|
215
|
+
# @return [true] always returns true
|
216
|
+
# @param [Hash] env Rack request environment
|
201
217
|
def update_cookie(env)
|
202
|
-
return unless @
|
203
|
-
return if env['global_session.req.update'] == false
|
218
|
+
return true unless @configuration['authority']
|
219
|
+
return true if env['global_session.req.update'] == false
|
204
220
|
|
205
221
|
session = env['global_session']
|
206
222
|
|
207
223
|
if session
|
208
224
|
unless session.valid?
|
209
225
|
old_session = session
|
210
|
-
session
|
226
|
+
session = @directory.create_session
|
211
227
|
perform_invalidation_callbacks(env, old_session, session)
|
212
228
|
env['global_session'] = session
|
213
229
|
end
|
214
230
|
|
215
|
-
value
|
231
|
+
value = session.to_s
|
216
232
|
expires = @configuration['ephemeral'] ? nil : session.expired_at
|
217
233
|
unless env['rack.cookies'][@cookie_name] == value
|
218
234
|
env['rack.cookies'][@cookie_name] =
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
235
|
+
{:value => value,
|
236
|
+
:domain => cookie_domain(env),
|
237
|
+
:expires => expires,
|
238
|
+
:httponly => true}
|
223
239
|
end
|
224
240
|
else
|
225
241
|
# write an empty cookie
|
226
242
|
wipe_cookie(env)
|
227
243
|
end
|
244
|
+
|
245
|
+
true
|
228
246
|
rescue Exception => e
|
229
247
|
wipe_cookie(env)
|
230
248
|
raise e
|
@@ -232,25 +250,27 @@ module GlobalSession
|
|
232
250
|
|
233
251
|
# Delete the global session cookie from the cookie jar.
|
234
252
|
#
|
235
|
-
#
|
236
|
-
#
|
253
|
+
# @return [true] always returns true
|
254
|
+
# @param [Hash] env Rack request environment
|
237
255
|
def wipe_cookie(env)
|
238
|
-
return unless @
|
256
|
+
return unless @configuration['authority']
|
239
257
|
return if env['global_session.req.update'] == false
|
240
258
|
|
241
|
-
env['rack.cookies'][@cookie_name] = {:value
|
242
|
-
:domain
|
259
|
+
env['rack.cookies'][@cookie_name] = {:value => nil,
|
260
|
+
:domain => cookie_domain(env),
|
243
261
|
:expires => Time.at(0)}
|
262
|
+
|
263
|
+
true
|
244
264
|
end
|
245
265
|
|
246
266
|
# Handle exceptions that occur during app invocation. This will either save the error
|
247
267
|
# in the Rack environment or raise it, depending on the type of error. The error may
|
248
268
|
# also be logged.
|
249
269
|
#
|
250
|
-
#
|
251
|
-
#
|
252
|
-
#
|
253
|
-
#
|
270
|
+
# @return [true] always returns true
|
271
|
+
# @param [String] activity name of activity during which the error happened
|
272
|
+
# @param [Hash] env Rack request environment
|
273
|
+
# @param [Exception] e error that happened
|
254
274
|
def handle_error(activity, env, e)
|
255
275
|
if env['rack.logger']
|
256
276
|
msg = "#{e.class} while #{activity}: #{e}"
|
@@ -264,17 +284,20 @@ module GlobalSession
|
|
264
284
|
elsif e.is_a? ConfigurationError
|
265
285
|
env['global_session.error'] = e
|
266
286
|
else
|
287
|
+
# Don't intercept errors unless they're GlobalSession-related
|
267
288
|
raise e
|
268
289
|
end
|
290
|
+
|
291
|
+
true
|
269
292
|
end
|
270
293
|
|
271
294
|
# Perform callbacks to directory and/or local session
|
272
295
|
# informing them that this session has been invalidated.
|
273
296
|
#
|
274
|
-
#
|
275
|
-
#
|
276
|
-
#
|
277
|
-
#
|
297
|
+
# @return [true] always returns true
|
298
|
+
# @param [Hash] env Rack request environment
|
299
|
+
# @param [GlobalSession::Session] old_session now-invalidated session
|
300
|
+
# @param [GlobalSession::Session] new_session new session that will be sent to the client
|
278
301
|
def perform_invalidation_callbacks(env, old_session, new_session)
|
279
302
|
if (local_session = env[LOCAL_SESSION_KEY]) && local_session.respond_to?(:rename!)
|
280
303
|
local_session.rename!(old_session, new_session)
|
@@ -287,16 +310,15 @@ module GlobalSession
|
|
287
310
|
# in the configuration if one is found; otherwise, uses the SERVER_NAME from the request
|
288
311
|
# but strips off the first component if the domain name contains more than two components.
|
289
312
|
#
|
290
|
-
#
|
291
|
-
# env(Hash):: the Rack environment hash
|
313
|
+
# @param [Hash] env Rack request environment
|
292
314
|
def cookie_domain(env)
|
293
|
-
if @configuration['cookie'].
|
315
|
+
if @configuration['cookie'].has_key?('domain')
|
294
316
|
# Use the explicitly provided domain name
|
295
317
|
domain = @configuration['cookie']['domain']
|
296
318
|
else
|
297
319
|
# Use the server name, but strip off the most specific component
|
298
|
-
parts
|
299
|
-
parts
|
320
|
+
parts = env['SERVER_NAME'].split('.')
|
321
|
+
parts = parts[1..-1] if parts.length > 2
|
300
322
|
domain = parts.join('.')
|
301
323
|
end
|
302
324
|
|