global_session 3.0.5 → 3.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|