global_session 3.2.10 → 3.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 277ed2049447151c9efbcbe7f0a53cc5860bcdd9
4
- data.tar.gz: 515d8873815c891c2e1b0bc03ec3ae0e26ecd333
3
+ metadata.gz: bdc242048b36de58af17de56b0b99eb35749402b
4
+ data.tar.gz: 21c79fa5ca975c6fd8dddaa623c00f24f385276f
5
5
  SHA512:
6
- metadata.gz: 7d19d6f8f399ae201e2969bf4c529549dc2250615cb82521d6edc3eaae63b3174de74aa66edf3afff9aedafc3922626977ddc9a207ab48b55869a5f4140f4ec5
7
- data.tar.gz: 8518212d89097547ac73c6ca7301d9db19e91ab22e24669b52d2f72a6f4f59fbcf2ca6a03777873d639b1adb0fb8666afaab44275ec7961efb4d2877c654977e
6
+ metadata.gz: b2030dc06623067a3ebbbdbdfda30d465e2d7f84c9cef19fbc9bff55f7ea11e65e656cd10854b0abf0e0f5ee2e6273a54076d99ff0fd85aaaac65fbd552e4bbd
7
+ data.tar.gz: b2d3249a309714a84b47699adb4bc7531c3a56aaa5a76ea3b2df81a4d2ee778ab75e439e2870089223c212d6f42254eb5874b9debcb92f4030272a987a7c05b1
@@ -1,91 +1,26 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
- # -*- encoding: utf-8 -*-
5
- # stub: global_session 3.2.9 ruby lib
1
+ # encoding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'global_session/version'
6
5
 
7
- Gem::Specification.new do |s|
8
- s.name = "global_session"
9
- s.version = "3.2.9"
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'global_session'
8
+ spec.version = GlobalSession::VERSION
9
+ spec.authors = ['Tony Spataro']
10
+ spec.email = 'rubygems@rightscale.com'
10
11
 
11
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
- s.require_paths = ["lib"]
13
- s.authors = ["Tony Spataro"]
14
- s.date = "2016-09-13"
15
- s.description = "This Rack middleware allows several web apps in an authentication domain to share session state, facilitating single sign-on in a distributed web app. It only provides session sharing and does not concern itself with authentication or replication of the user database."
16
- s.email = "support@rightscale.com"
17
- s.extra_rdoc_files = [
18
- "LICENSE",
19
- "README.rdoc"
20
- ]
21
- s.files = [
22
- ".ruby-version",
23
- ".travis.yml",
24
- "CHANGELOG.md",
25
- "LICENSE",
26
- "README.rdoc",
27
- "Rakefile",
28
- "VERSION",
29
- "global_session.gemspec",
30
- "init.rb",
31
- "lib/global_session.rb",
32
- "lib/global_session/configuration.rb",
33
- "lib/global_session/directory.rb",
34
- "lib/global_session/encoding.rb",
35
- "lib/global_session/keystore.rb",
36
- "lib/global_session/rack.rb",
37
- "lib/global_session/rails.rb",
38
- "lib/global_session/rails/action_controller_class_methods.rb",
39
- "lib/global_session/rails/action_controller_instance_methods.rb",
40
- "lib/global_session/session.rb",
41
- "lib/global_session/session/abstract.rb",
42
- "lib/global_session/session/v1.rb",
43
- "lib/global_session/session/v2.rb",
44
- "lib/global_session/session/v3.rb",
45
- "rails/init.rb",
46
- "rails_generators/global_session/USAGE",
47
- "rails_generators/global_session/global_session_generator.rb",
48
- "rails_generators/global_session/templates/global_session.yml.erb",
49
- "rails_generators/global_session_authority/USAGE",
50
- "rails_generators/global_session_authority/global_session_authority_generator.rb"
51
- ]
52
- s.homepage = "https://github.com/rightscale/global_session"
53
- s.licenses = ["MIT"]
54
- s.required_ruby_version = Gem::Requirement.new("~> 2.0")
55
- s.rubygems_version = "2.2.3"
56
- s.summary = "Secure single-domain session sharing plugin for Rack and Rails."
12
+ spec.summary = 'Reusable foundation code.'
13
+ spec.description = 'A toolkit of useful, reusable foundation code created by RightScale.'
14
+ spec.homepage = 'https://github.com/rightscale/right_support'
15
+ spec.license = 'MIT'
57
16
 
58
- if s.respond_to? :specification_version then
59
- s.specification_version = 4
17
+ spec.files = `git ls-files -z`.split("\x0").select { |f| f.match(%r{lib/|gemspec}) }
18
+ spec.require_paths = ['lib']
60
19
 
61
- if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
62
- s.add_runtime_dependency(%q<json>, ["~> 1.4"])
63
- s.add_runtime_dependency(%q<rack-contrib>, ["~> 1.0"])
64
- s.add_runtime_dependency(%q<right_support>, ["< 4.0", ">= 2.8.2"])
65
- s.add_runtime_dependency(%q<simple_uuid>, [">= 0.2.0"])
66
- s.add_development_dependency(%q<ruby-debug>, ["~> 0.10"])
67
- s.add_development_dependency(%q<pry>, ["~> 0.10"])
68
- s.add_development_dependency(%q<pry-byebug>, ["~> 2.0"])
69
- s.add_development_dependency(%q<jeweler>, ["~> 1.8"])
70
- else
71
- s.add_dependency(%q<json>, ["~> 1.4"])
72
- s.add_dependency(%q<rack-contrib>, ["~> 1.0"])
73
- s.add_dependency(%q<right_support>, ["< 4.0", ">= 2.8.2"])
74
- s.add_dependency(%q<simple_uuid>, [">= 0.2.0"])
75
- s.add_dependency(%q<ruby-debug>, ["~> 0.10"])
76
- s.add_dependency(%q<pry>, ["~> 0.10"])
77
- s.add_dependency(%q<pry-byebug>, ["~> 2.0"])
78
- s.add_dependency(%q<jeweler>, ["~> 1.8"])
79
- end
80
- else
81
- s.add_dependency(%q<json>, ["~> 1.4"])
82
- s.add_dependency(%q<rack-contrib>, ["~> 1.0"])
83
- s.add_dependency(%q<right_support>, ["< 4.0", ">= 2.8.2"])
84
- s.add_dependency(%q<simple_uuid>, [">= 0.2.0"])
85
- s.add_dependency(%q<ruby-debug>, ["~> 0.10"])
86
- s.add_dependency(%q<pry>, ["~> 0.10"])
87
- s.add_dependency(%q<pry-byebug>, ["~> 2.0"])
88
- s.add_dependency(%q<jeweler>, ["~> 1.8"])
89
- end
90
- end
20
+ spec.required_ruby_version = Gem::Requirement.new('~> 2.1')
91
21
 
22
+ spec.add_runtime_dependency('json', ['~> 1.4'])
23
+ spec.add_runtime_dependency('rack-contrib', ['~> 1.0'])
24
+ spec.add_runtime_dependency('right_support', ['>= 2.14.1', '< 3.0'])
25
+ spec.add_runtime_dependency('simple_uuid', ['>= 0.2.0'])
26
+ end
@@ -27,24 +27,26 @@ module GlobalSession
27
27
  # The general category of client-side errors. Used solely as a base class.
28
28
  class ClientError < Exception; end
29
29
 
30
- # Indicates that the global session configuration file is missing from disk.
30
+ # Global session configuration is missing from the environment or filesystem.
31
31
  #
32
32
  class MissingConfiguration < ConfigurationError; end
33
33
 
34
- # Indicates that a client submitted a request with a valid session cookie, but the
35
- # session ID was reported as invalid by the Directory.
34
+ # The request has a valid session cookie, but the session ID was reported as
35
+ # invalid by the Directory.
36
36
  #
37
37
  # See Directory#valid_session? for more information.
38
38
  #
39
39
  class InvalidSession < ClientError; end
40
40
 
41
- # Indicates that a client submitted a request with a valid session cookie, but the
42
- # session has expired.
41
+ # The request has a valid session cookie, but the session has expired.
43
42
  #
44
43
  class ExpiredSession < ClientError; end
45
44
 
46
- # Indicates that a client submitted a request with a session cookie that could not
47
- # be decoded or decompressed.
45
+ # The request has a valid session cookie, but the session has expired.
46
+ class PrematureSession < ExpiredSession; end
47
+
48
+ # The request has a session cookie, but the cookie is malformed and cannot be
49
+ # interpreted as session state.
48
50
  #
49
51
  class MalformedCookie < ClientError
50
52
  attr_reader :cookie
@@ -76,6 +78,9 @@ module GlobalSession
76
78
  # information.
77
79
  #
78
80
  class NoAuthority < ConfigurationError; end
81
+
82
+ # The request has a session cookie, but its signature is invalid.
83
+ class InvalidSignature < SecurityError; end
79
84
  end
80
85
 
81
86
  #Make sure gem dependencies are activated.
@@ -28,7 +28,7 @@ module GlobalSession
28
28
  # The default implementation is simplistic, but should be suitable for most applications.
29
29
  # Directory is designed to be specialized via subclassing. To override the behavior to
30
30
  # suit your needs, simply create a subclass of Directory and add a configuration file
31
- # setting to specify the class name of your implementation:
31
+ # setting to specify the class name of your implementation:
32
32
  #
33
33
  # common:
34
34
  # directory:
@@ -108,21 +108,23 @@ module GlobalSession
108
108
  # InvalidSession:: if the session contained in the cookie has been invalidated
109
109
  # ExpiredSession:: if the session contained in the cookie has expired
110
110
  # MalformedCookie:: if the cookie was corrupt or malformed
111
- # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
111
+ # InvalidSignature:: if signature is invalid or cookie is not signed by a trusted authority
112
112
  def create_session(cookie=nil)
113
113
  forced_version = configuration['cookie']['version']
114
114
 
115
115
  if cookie.nil?
116
116
  # Create a legitimately new session
117
117
  case forced_version
118
- when 3, nil
118
+ when 4
119
+ Session::V4.new(self, cookie)
120
+ when nil, 3
119
121
  Session::V3.new(self, cookie)
120
122
  when 2
121
123
  Session::V2.new(self, cookie)
122
124
  when 1
123
125
  Session::V1.new(self, cookie)
124
126
  else
125
- raise ArgumentError, "Unknown value #{forced_version} for configuration.cookie.version"
127
+ raise ArgumentError, "Unknown value #{forced_version} for configuration.cookie.version"
126
128
  end
127
129
  else
128
130
  warn "GlobalSession::Directory#create_session with an existing session is DEPRECATED -- use #load_session instead"
@@ -142,7 +144,7 @@ module GlobalSession
142
144
  # InvalidSession:: if the session contained in the cookie has been invalidated
143
145
  # ExpiredSession:: if the session contained in the cookie has expired
144
146
  # MalformedCookie:: if the cookie was corrupt or malformed
145
- # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
147
+ # InvalidSignature:: if signature is invalid or cookie is not signed by a trusted authority
146
148
  def load_session(cookie)
147
149
  Session.new(self, cookie)
148
150
  end
@@ -170,7 +172,7 @@ module GlobalSession
170
172
  def local_authority_name
171
173
  @keystore.private_key_name
172
174
  end
173
-
175
+
174
176
  # Determine whether this system trusts a particular named authority based on
175
177
  # the settings specified in Configuration and/or the presence of public key
176
178
  # files on disk.
@@ -64,11 +64,23 @@ module GlobalSession
64
64
 
65
65
  # Factory method to generate a new keypair for use with GlobalSession.
66
66
  #
67
+ # @param [Integer,String] parameter keylength in bits (for RSA/DSA) or curve name (for EC)
67
68
  # @raise [ArgumentError] if cryptosystem is unknown to OpenSSL
68
69
  # @return [OpenSSL::PKey::PKey] a public/private keypair
69
- def self.create_keypair(cryptosystem=:RSA, keysize=1024)
70
+ def self.create_keypair(cryptosystem=:RSA, parameter=nil)
70
71
  factory = OpenSSL::PKey.const_get(cryptosystem)
71
- factory.generate( 1024 )
72
+ if factory.respond_to?(:generate)
73
+ # parameter-free cryptosystem e.g. RSA, DSA. Default key length 1024,
74
+ # which is really too small, but whose signatures are quite large.
75
+ parameter ||= 1024
76
+ factory.generate( parameter )
77
+ else
78
+ # parameterized family of cryptosystems (e.g. EC). Default curve is
79
+ # compatible with JSON Web Signature (JWS) ES256 algorithm.
80
+ parameter ||= 'prime256v1'
81
+ alg = factory.new(parameter)
82
+ alg.generate_key
83
+ end
72
84
  rescue NameError => e
73
85
  raise ArgumentError, e.message
74
86
  end
@@ -106,9 +118,17 @@ module GlobalSession
106
118
  end
107
119
  elsif File.file?(path)
108
120
  name = File.basename(path, '.*')
109
- key = OpenSSL::PKey::RSA.new(File.read(path))
121
+ pem = File.read(path)
122
+
123
+ # Deal with modern ("BEGIN PUBLIC/PRIVATE KEY") and legacy ("BEGIN RSA PUBLIC KEY") formats
124
+ if pem =~ /BEGIN RSA/
125
+ key = OpenSSL::PKey::RSA.new(pem)
126
+ else
127
+ key = OpenSSL::PKey.read(pem)
128
+ end
129
+
110
130
  # ignore private keys (which legacy config allowed to coexist with public keys)
111
- unless key.private?
131
+ unless (key.private? rescue nil) || (key.private_key? rescue nil)
112
132
  if @public_keys.has_key?(name)
113
133
  raise ConfigurationError, "Duplicate public key for authority: #{name}"
114
134
  else
@@ -135,8 +155,10 @@ module GlobalSession
135
155
  if File.file?(path)
136
156
  if @private_key.nil?
137
157
  name = File.basename(path, '.*')
138
- private_key = OpenSSL::PKey::RSA.new(File.read(path))
139
- raise ConfigurationError, "Expected #{key_file} to contain an RSA private key" unless private_key.private?
158
+ pem = File.read(path)
159
+ private_key = OpenSSL::PKey.read(pem)
160
+
161
+ raise ConfigurationError, "Expected #{key_file} to contain an RSA private key" unless (private_key.private? rescue nil) || (private_key.private_key? rescue nil)
140
162
  @private_key = private_key
141
163
  @private_key_name = name
142
164
  else
@@ -298,7 +298,7 @@ module GlobalSession
298
298
  env['rack.logger'].error(msg)
299
299
  end
300
300
 
301
- if e.is_a?(ClientError) || e.is_a?(SecurityError)
301
+ if e.is_a?(ClientError) || e.is_a?(InvalidSignature)
302
302
  env['global_session.error'] = e
303
303
  wipe_cookie(env)
304
304
  elsif e.is_a? ConfigurationError
@@ -7,6 +7,7 @@ require 'global_session/session/abstract'
7
7
  require 'global_session/session/v1'
8
8
  require 'global_session/session/v2'
9
9
  require 'global_session/session/v3'
10
+ require 'global_session/session/v4'
10
11
 
11
12
  # Ladies and gentlemen: the one and only, star of the show, GLOBAL SESSION!
12
13
  #
@@ -32,20 +33,24 @@ module GlobalSession::Session
32
33
 
33
34
  # Decode a global session cookie. Use a heuristic to determine the version.
34
35
  # @raise [GlobalSession::MalformedCookie] if the cookie is not a valid serialized global session
35
- def self.new(directory, cookie=nil, valid_signature_digest=nil)
36
+ def self.new(directory, cookie=nil)
36
37
  guess_version(cookie).new(directory, cookie)
37
38
  end
38
39
 
40
+ # Figure out the protocol version of a serialized session cookie.
41
+ #
42
+ # @param [String] cookie
43
+ # @return [Class] implementation class that can probably deserialize cookie
39
44
  def self.guess_version(cookie)
40
45
  case cookie
41
- when /^WzM/ # == "[3"
46
+ when V4::HEADER
47
+ V4
48
+ when nil, V3::HEADER
42
49
  V3
43
- when /^l9/ # == binary msgpack symbol for "beginning of array"
50
+ when V2::HEADER
44
51
  V2
45
- when /^eN/ # == zlib-compressed form of "{"
46
- V1
47
52
  else
48
- V1 # due to zlib compression, there might be corner cases with the eN prefix
53
+ V1 # due to zlib compression, no foolproof way to spot V1 sessoins
49
54
  end
50
55
  end
51
56
  end
@@ -14,7 +14,7 @@ module GlobalSession::Session
14
14
  # InvalidSession:: if the session contained in the cookie has been invalidated
15
15
  # ExpiredSession:: if the session contained in the cookie has expired
16
16
  # MalformedCookie:: if the cookie was corrupt or malformed
17
- # SecurityError:: if signature is invalid or cookie is not signed by a trusted authority
17
+ # InvalidSignature:: if signature is invalid or cookie is not signed by a trusted authority
18
18
  def initialize(directory, cookie=nil)
19
19
  @directory = directory
20
20
  @signed = {}
@@ -75,6 +75,80 @@ module GlobalSession::Session
75
75
  @cookie.nil?
76
76
  end
77
77
 
78
+ # Return the keys that are currently present in the global session.
79
+ #
80
+ # === Return
81
+ # keys(Array):: List of keys contained in the global session
82
+ def keys
83
+ @signed.keys + @insecure.keys
84
+ end
85
+
86
+ # Return the values that are currently present in the global session.
87
+ #
88
+ # === Return
89
+ # values(Array):: List of values contained in the global session
90
+ def values
91
+ @signed.values + @insecure.values
92
+ end
93
+
94
+ # Iterate over each key/value pair
95
+ #
96
+ # === Block
97
+ # An iterator which will be called with each key/value pair
98
+ #
99
+ # === Return
100
+ # Returns the value of the last expression evaluated by the block
101
+ def each_pair(&block) # :yields: |key, value|
102
+ @signed.each_pair(&block)
103
+ @insecure.each_pair(&block)
104
+ end
105
+
106
+ # Lookup a value by its key.
107
+ #
108
+ # === Parameters
109
+ # key(String):: the key
110
+ #
111
+ # === Return
112
+ # value(Object):: The value associated with +key+, or nil if +key+ is not present
113
+ def [](key)
114
+ key = key.to_s #take care of symbol-style keys
115
+ @signed[key] || @insecure[key]
116
+ end
117
+
118
+ # Set a value in the global session hash. If the supplied key is denoted as
119
+ # secure by the global session schema, causes a new signature to be computed
120
+ # when the session is next serialized.
121
+ #
122
+ # === Parameters
123
+ # key(String):: The key to set
124
+ # value(Object):: The value to set
125
+ #
126
+ # === Return
127
+ # value(Object):: Always returns the value that was set
128
+ #
129
+ # ===Raise
130
+ # InvalidSession:: if the session has been invalidated (and therefore can't be written to)
131
+ # ArgumentError:: if the configuration doesn't define the specified key as part of the global session
132
+ # NoAuthority:: if the specified key is secure and the local node is not an authority
133
+ # UnserializableType:: if the specified value can't be serialized as JSON
134
+ def []=(key, value)
135
+ key = key.to_s #take care of symbol-style keys
136
+ raise GlobalSession::InvalidSession unless valid?
137
+
138
+ if @schema_signed.include?(key)
139
+ authority_check
140
+ @signed[key] = value
141
+ @dirty_secure = true
142
+ elsif @schema_insecure.include?(key)
143
+ @insecure[key] = value
144
+ @dirty_insecure = true
145
+ else
146
+ raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
147
+ end
148
+
149
+ return value
150
+ end
151
+
78
152
  # Determine whether the session is valid. This method simply delegates to the
79
153
  # directory associated with this session.
80
154
  #
@@ -88,7 +162,7 @@ module GlobalSession::Session
88
162
  #
89
163
  # @return [Boolean] true if something has changed
90
164
  def dirty?
91
- !!(new_record? || @dirty_timestamps)
165
+ !!(new_record? || @dirty_timestamps || @dirty_secure || @dirty_insecure)
92
166
  end
93
167
 
94
168
  # Determine whether the global session schema allows a given key to be placed
@@ -116,6 +190,34 @@ module GlobalSession::Session
116
190
 
117
191
  alias key? has_key?
118
192
 
193
+ # Delete a key from the global session attributes. If the key exists,
194
+ # mark the global session dirty
195
+ #
196
+ # @param [String] the key to delete
197
+ # @return [Object] the value of the key deleted, or nil if not found
198
+ def delete(key)
199
+ key = key.to_s #take care of symbol-style keys
200
+ raise GlobalSession::InvalidSession unless valid?
201
+
202
+ if @schema_signed.include?(key)
203
+ authority_check
204
+
205
+ # Only mark dirty if the key actually exists
206
+ @dirty_secure = true if @signed.keys.include? key
207
+ value = @signed.delete(key)
208
+ elsif @schema_insecure.include?(key)
209
+
210
+ # Only mark dirty if the key actually exists
211
+ @dirty_insecure = true if @insecure.keys.include? key
212
+ value = @insecure.delete(key)
213
+ else
214
+ raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
215
+ end
216
+
217
+ return value
218
+ end
219
+
220
+
119
221
  # Invalidate this session by reporting its UUID to the Directory.
120
222
  #
121
223
  # === Return
@@ -140,6 +242,10 @@ module GlobalSession::Session
140
242
 
141
243
  private
142
244
 
245
+ def generate_id
246
+ RightSupport::Data::Base64URL.encode(SecureRandom.random_bytes(8))
247
+ end
248
+
143
249
  def authority_check # :nodoc:
144
250
  unless @directory.local_authority_name
145
251
  raise GlobalSession::NoAuthority, 'Cannot change secure session attributes; we are not an authority'
@@ -155,7 +261,20 @@ module GlobalSession::Session
155
261
  end
156
262
 
157
263
  def create_invalid
158
- raise NotImplementedError, "Subclass responsibility"
264
+ @id = nil
265
+ @created_at = Time.now.utc
266
+ @expired_at = created_at
267
+ @signed = {}
268
+ @insecure = {}
269
+ @authority = nil
270
+ end
271
+
272
+ # This is called by Object#clone and is used to augment our "shallow clone"
273
+ # behavior so that we don't share state hashes between clones.
274
+ def initialize_copy(source)
275
+ super
276
+ @signed = ::RightSupport::Data::HashTools.deep_clone2(@signed)
277
+ @insecure = ::RightSupport::Data::HashTools.deep_clone2(@insecure)
159
278
  end
160
279
  end
161
- end
280
+ end