global_session 3.2.10 → 3.3.0

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