global_session 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +179 -0
- data/global_session.gemspec +37 -0
- data/init.rb +4 -0
- data/lib/global_session/configuration.rb +126 -0
- data/lib/global_session/directory.rb +103 -0
- data/lib/global_session/encoding.rb +70 -0
- data/lib/global_session/integrated_session.rb +123 -0
- data/lib/global_session/rack.rb +162 -0
- data/lib/global_session/rails/action_controller_class_methods.rb +61 -0
- data/lib/global_session/rails/action_controller_instance_methods.rb +135 -0
- data/lib/global_session/rails.rb +30 -0
- data/lib/global_session/session.rb +343 -0
- data/lib/global_session.rb +63 -0
- data/rails/init.rb +3 -0
- data/rails_generators/global_session_authority/USAGE +1 -0
- data/rails_generators/global_session_authority/global_session_authority_generator.rb +32 -0
- data/rails_generators/global_session_config/USAGE +1 -0
- data/rails_generators/global_session_config/global_session_config_generator.rb +19 -0
- data/rails_generators/global_session_config/templates/global_session.yml.erb +49 -0
- metadata +214 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>
|
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.
|
data/README.rdoc
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
HasGlobalSession enables multiple heterogeneous Web applications to share
|
4
|
+
session state in a cryptographically secure way, facilitating single sign-on
|
5
|
+
and enabling easier development of large-scale distributed applications that
|
6
|
+
make use of architectural strategies such as sharding or separation of concerns.
|
7
|
+
|
8
|
+
In other words: it lets semi-related Web apps share selected bits of session
|
9
|
+
state.
|
10
|
+
|
11
|
+
This plugin does not provide a complete solution for identity management. In
|
12
|
+
particular, it does not provide any of the following:
|
13
|
+
|
14
|
+
* <b>federation</b> -- aka cross-domain single sign-on -- use OpenID for that.
|
15
|
+
|
16
|
+
* <b>authentication</b> -- the application must authenticate the user.
|
17
|
+
|
18
|
+
* <b>authorization</b> -- the application is responsible for using the contents
|
19
|
+
of the global session to make authorization decisions.
|
20
|
+
|
21
|
+
* <b>secrecy</b> -- global session attributes can be signed but never encrypted;
|
22
|
+
protect against third-party snooping using SSL. Group secrecy is expensive;
|
23
|
+
if you don't want your users to see their session state, put it in a database,
|
24
|
+
or in an encrypted local session cookie.
|
25
|
+
|
26
|
+
* <b>replication</b> -- the authentication authorities must have some way to
|
27
|
+
share information about the database of users in order to authenticate
|
28
|
+
them and place identifying information into the global session.
|
29
|
+
|
30
|
+
* <b>single sign-out</b> -- the authorities must have some way to broadcast a
|
31
|
+
notification when sessions are invalidated; they can override the default
|
32
|
+
Directory implementation to do realtime revocation checking.
|
33
|
+
|
34
|
+
= Example
|
35
|
+
|
36
|
+
1) Create a basic config file and edit it to suit your needs:
|
37
|
+
$ script/generate global_session_config mycoolapp.com
|
38
|
+
|
39
|
+
2) Create an authentication authority:
|
40
|
+
$ script/generate global_session_authority mycoolapp
|
41
|
+
|
42
|
+
3) Declare that some or all of your controllers will use the global session:
|
43
|
+
class ApplicationController < ActionController::Base
|
44
|
+
has_global_session
|
45
|
+
end
|
46
|
+
|
47
|
+
4) Make use of the global session hash in your controllers:
|
48
|
+
global_session['user'] = @user.id
|
49
|
+
...
|
50
|
+
@current_user = User.find(global_session['user'])
|
51
|
+
|
52
|
+
5) For easier programming, enable seamless integration with the local session:
|
53
|
+
(in global_session.yml)
|
54
|
+
common:
|
55
|
+
integrated: true
|
56
|
+
|
57
|
+
(in your controllers)
|
58
|
+
session['user'] = @user.id #goes to the global session
|
59
|
+
session['local_thingie'] = @thingie.id #goes to the local session
|
60
|
+
|
61
|
+
= Global Session Contents
|
62
|
+
|
63
|
+
Global session state is stored as a cookie in the user's browser. The cookie
|
64
|
+
is a Zlib-compressed JSON dictionary containing the following stuff:
|
65
|
+
* session metadata (UUID, created-at, expires-at, signing-authority)
|
66
|
+
* signed session attributes (e.g. the authenticated user ID)
|
67
|
+
* insecure session attributes (e.g. the last-visited URL)
|
68
|
+
* a cryptographic signature of the metadata and signed attributes
|
69
|
+
|
70
|
+
The global session is unserialized and its signature is verified whenever
|
71
|
+
a controller asks for one of its attributes. The cookie's value is updated
|
72
|
+
whenever attributes change. As an optimization, the signature is only
|
73
|
+
recomputed when the metadata or signed attributes have changed; insecure
|
74
|
+
attributes can change "for free."
|
75
|
+
|
76
|
+
Because the security properties of attributes can vary, HasGlobalSession
|
77
|
+
requires all _possible_ attributes to be declared up-front in the config
|
78
|
+
file. The 'attributes' section of the config file defines the _schema_
|
79
|
+
for the global session: which attributes can be used, which can be trusted
|
80
|
+
to make authorization decisions (because they are signed), and are insecure
|
81
|
+
and therefore act only as "hints" about the session.
|
82
|
+
|
83
|
+
Since the session is serialized as JSON, only a limited range of object
|
84
|
+
types can be stored in it: strings, numbers, lists, hashes and other Ruby
|
85
|
+
primitives. Ruby booleans (true/false) do not translate well into JSON
|
86
|
+
and should be avoided.
|
87
|
+
|
88
|
+
= Detailed Information
|
89
|
+
|
90
|
+
== Global Session Domain
|
91
|
+
|
92
|
+
We refer to collection of _all_ Web application instances capable of using the
|
93
|
+
global session as the "domain." The global session domain may consist of any
|
94
|
+
number of distinct nodes, possibly hidden behind load balancers or proxies.
|
95
|
+
The nodes within the domain may all be running the same Rails application,
|
96
|
+
or they may be running different codebases that represent different parts of
|
97
|
+
a distributed application. (They may also be using app frameworks other than
|
98
|
+
Rails.)
|
99
|
+
|
100
|
+
The only constraint imposed by HasGlobalSession is that all nodes within the
|
101
|
+
domain must have end-user-facing URLs within the same second-level DNS domain.
|
102
|
+
This is due to limitations imposed by the HTTP cookie mechanism: for privacy
|
103
|
+
reasons, cookies will only be sent to nodes within the same domain as the
|
104
|
+
node that first created them.
|
105
|
+
|
106
|
+
For example, in my HasGlobalSession configuration file I might specify that my
|
107
|
+
cookie's domain is "example.com". My app nodes at app1.example.com and
|
108
|
+
app2.example.com would be part of the global session domain, but my business
|
109
|
+
partner's application at app3.partner.com could not participate.
|
110
|
+
|
111
|
+
== Authorities and Relying Parties
|
112
|
+
|
113
|
+
A node that can create or update the global session is said to be an "authority"
|
114
|
+
(because it's trusted by other parties to make assertions about global session
|
115
|
+
state). An application that can read the global session is said to be a "relying
|
116
|
+
party." In practice, every application is a relying party, but not all of them
|
117
|
+
need to be authorities.
|
118
|
+
|
119
|
+
There is an RSA key pair associated with each authority. The authority's
|
120
|
+
public key is distribued to all relying parties, but the private key must
|
121
|
+
remain a secret to that authority (which may consist of many individual
|
122
|
+
nodes).
|
123
|
+
|
124
|
+
This system allows for significant flexibility when configuring a distributed
|
125
|
+
app's global session. There must be at least one authority, but for many apps
|
126
|
+
one authority (plus an arbitrary number of relying parties, which do not need
|
127
|
+
a key pair) will be sufficient.
|
128
|
+
|
129
|
+
In general, two systems should be part of the same authority if there is no
|
130
|
+
trust boundary between them -- that is to say, trust between the two systems
|
131
|
+
is unlimited in both directions.
|
132
|
+
|
133
|
+
Here are some reasons you might consider dividing your systems into different
|
134
|
+
authorities:
|
135
|
+
* beta/staging system vs. production system
|
136
|
+
* system hosted by a third party vs. system hosted internally
|
137
|
+
* e-commerce node vs. storefront node vs. admin node
|
138
|
+
|
139
|
+
== The Directory
|
140
|
+
|
141
|
+
The Directory is a Ruby object instantiated by HasGlobalSession in order to
|
142
|
+
perform lookups of public and private keys. Given an authority name (as found
|
143
|
+
in a session cookie), the Directory can find the corresponding public key.
|
144
|
+
|
145
|
+
If the local system is an authority itself, #local_authority_name will
|
146
|
+
return non-nil and #private_key will return a private key suitable for
|
147
|
+
signing session attributes.
|
148
|
+
|
149
|
+
The Directory implementation included with HasGlobalSession uses the filesystem
|
150
|
+
as the backing store for its key pairs. Its #initialize method accepts a
|
151
|
+
filesystem path that will be searched for files containing PEM-encoded public
|
152
|
+
and private keys (the same format used by OpenSSH). This simple Directory
|
153
|
+
implementation relies on the following conventions:
|
154
|
+
* Public keys have a *.pub extension.
|
155
|
+
* Private keys have a *.key extension.
|
156
|
+
* If a node is an authority, then one (and *only* one) *.key file should exist.
|
157
|
+
* The local node's authority name is inferred from the name of the private key
|
158
|
+
file.
|
159
|
+
|
160
|
+
When used with a Rails app, HasGlobalSession expects to find its keystore in
|
161
|
+
config/authorities. You can use the global_session generator to create new key
|
162
|
+
pairs. Remember never to check a *.key file into a public repository!! (*.pub
|
163
|
+
files can be checked into source control and distributed freely.)
|
164
|
+
|
165
|
+
If you wish all of the systems to stop trusting an authority, simply delete
|
166
|
+
its public key from config/authorities and re-deploy your app.
|
167
|
+
|
168
|
+
=== Implementing Your Own Directory Provider
|
169
|
+
|
170
|
+
To replace or enhance the built-in Directory, simply create a new class that
|
171
|
+
extends Directory and put the class somewhere in your app (the lib directory
|
172
|
+
is a good choice). In the HasGlobalSession configuration file, specify the
|
173
|
+
class name of the directory under the 'common' section, like so:
|
174
|
+
|
175
|
+
common:
|
176
|
+
integrated: true
|
177
|
+
directory: MyCoolDirectory
|
178
|
+
|
179
|
+
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# -*- mode: ruby; encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
spec = Gem::Specification.new do |s|
|
6
|
+
s.required_rubygems_version = nil if s.respond_to? :required_rubygems_version=
|
7
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
|
+
|
9
|
+
s.name = 'global_session'
|
10
|
+
s.version = '0.9.0'
|
11
|
+
s.date = '2010-12-07'
|
12
|
+
|
13
|
+
s.authors = ['Tony Spataro']
|
14
|
+
s.email = 'code@tracker.xeger.net'
|
15
|
+
s.homepage= 'http://github.com/xeger/global_session'
|
16
|
+
|
17
|
+
s.summary = %q{Secure single-domain session sharing plugin for Rails.}
|
18
|
+
s.description = %q{This plugin for Rails 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.}
|
19
|
+
|
20
|
+
s.add_runtime_dependency('uuidtools', [">= 1.0.7"])
|
21
|
+
s.add_runtime_dependency('json', [">= 1.1.7"])
|
22
|
+
s.add_runtime_dependency('rack-contrib', [">= 1.0"])
|
23
|
+
|
24
|
+
s.add_development_dependency('rake', ["~> 0.8.7"])
|
25
|
+
s.add_development_dependency('ruby-debug', ["~> 0.10.3"])
|
26
|
+
s.add_development_dependency('rspec', [">= 1.3.0"])
|
27
|
+
s.add_development_dependency('flexmock', [">= 0.8.6"])
|
28
|
+
s.add_development_dependency('actionpack', [">= 2.1.2"])
|
29
|
+
|
30
|
+
basedir = File.dirname(__FILE__)
|
31
|
+
candidates = ['global_session.gemspec', 'init.rb', 'MIT-LICENSE', 'README.rdoc'] +
|
32
|
+
Dir['lib/**/*'] +
|
33
|
+
Dir['rails/**/*'] +
|
34
|
+
Dir['rails/**/*'] +
|
35
|
+
Dir['rails_generators/**/*']
|
36
|
+
s.files = candidates.sort
|
37
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
module GlobalSession
|
2
|
+
# Central point of access for GlobalSession configuration information. This is
|
3
|
+
# mostly a very thin wrapper around the serialized hash written to the YAML config
|
4
|
+
# file.
|
5
|
+
#
|
6
|
+
# The configuration is stored as a set of nested hashes and accessed by the code
|
7
|
+
# using hash lookup; for example, we might ask for +Configuration['cookie']['domain']+
|
8
|
+
# if we wanted to know which domain the cookie should be set for.
|
9
|
+
#
|
10
|
+
# The following settings are supported:
|
11
|
+
# * attributes
|
12
|
+
# * signed
|
13
|
+
# * insecure
|
14
|
+
# * integrated
|
15
|
+
# * ephemeral
|
16
|
+
# * timeout
|
17
|
+
# * renew
|
18
|
+
# * authority
|
19
|
+
# * trust
|
20
|
+
# * directory
|
21
|
+
# * cookie
|
22
|
+
# * name
|
23
|
+
# * domain
|
24
|
+
#
|
25
|
+
# === Config Environments
|
26
|
+
# The operational environment of global_session defines which section
|
27
|
+
# of the configuration file it gets its settings from. When used with
|
28
|
+
# a web app, the environment should be set to the same environment as
|
29
|
+
# the web app. (If using Rails integration, this happens for you
|
30
|
+
# automatically.)
|
31
|
+
#
|
32
|
+
# === Environment-Specific Settings
|
33
|
+
# The top level of keys in the configuration hash are special; they provide different
|
34
|
+
# sections of settings that apply in different environments. For instance, a Rails
|
35
|
+
# application might have one set of settings that apply in the development environment;
|
36
|
+
# these would appear under +Configuration['development']+. Another set of settings would
|
37
|
+
# apply in the production environment and would appear under +Configuration['production']+.
|
38
|
+
#
|
39
|
+
# === Common Settings
|
40
|
+
# In addition to having one section for each operating environment, the configuration
|
41
|
+
# file can specify a 'common' section for settings that apply
|
42
|
+
#
|
43
|
+
# === Lookup Mechanism
|
44
|
+
# When the code asks for +Configuration['foo']+, we first check whether the current
|
45
|
+
# environment's config section has a value for foo. If one is found, we return that.
|
46
|
+
#
|
47
|
+
# If no environment-specific setting is found, we check the 'common' section and return
|
48
|
+
# the value found there.
|
49
|
+
#
|
50
|
+
# === Config File Location
|
51
|
+
# The name and location of the config file depend on the Web framework with which
|
52
|
+
# you are integrating; see GlobalSession::Rails for more information.
|
53
|
+
#
|
54
|
+
class Configuration
|
55
|
+
# Create a new Configuration objectt
|
56
|
+
#
|
57
|
+
# === Parameters
|
58
|
+
# config_File(String):: Absolute filesystem path to the configuration file
|
59
|
+
# environment(String):: Config file section from which to read settings
|
60
|
+
#
|
61
|
+
# === Raise
|
62
|
+
# MissingConfiguration:: if config file is missing or unreadable
|
63
|
+
# TypeError:: if config file does not contain a YAML-serialized Hash
|
64
|
+
def initialize(config_file, environment)
|
65
|
+
@config_file = config_file
|
66
|
+
@environment = environment
|
67
|
+
raise MissingConfiguration, "Missing or unreadable configuration file" unless File.readable?(@config_file)
|
68
|
+
@config = YAML.load(File.read(@config_file))
|
69
|
+
raise TypeError, "#{config_file} must contain a Hash!" unless Hash === @config
|
70
|
+
validate
|
71
|
+
end
|
72
|
+
|
73
|
+
# Reader for configuration elements. The reader first checks
|
74
|
+
# the current environment's settings section for the named
|
75
|
+
# value; if not found, it checks the common settings section.
|
76
|
+
#
|
77
|
+
# === Parameters
|
78
|
+
# key(String):: Name of configuration element to retrieve
|
79
|
+
#
|
80
|
+
# === Return
|
81
|
+
# value(String):: the value of the configuration element
|
82
|
+
def [](key)
|
83
|
+
get(key, true)
|
84
|
+
end
|
85
|
+
|
86
|
+
def validate # :nodoc
|
87
|
+
['attributes/signed', 'integrated', 'cookie/name',
|
88
|
+
'timeout'].each {|k| validate_presence_of k}
|
89
|
+
end
|
90
|
+
|
91
|
+
protected
|
92
|
+
|
93
|
+
# Helper method to check the presence of a key. Used in #validate.
|
94
|
+
#
|
95
|
+
# === Parameters
|
96
|
+
# key(String):: key name; for nested hashes, separate keys with /
|
97
|
+
#
|
98
|
+
# === Return
|
99
|
+
# true always
|
100
|
+
def validate_presence_of(key)
|
101
|
+
elements = key.split '/'
|
102
|
+
object = get(elements.shift, false)
|
103
|
+
elements.each do |element|
|
104
|
+
object = object[element] if object
|
105
|
+
if object.nil?
|
106
|
+
msg = "#{File.basename(@config_file)} does not specify required element #{elements.map { |x| "['#{x}']"}.join('')}"
|
107
|
+
raise MissingConfiguration, msg
|
108
|
+
end
|
109
|
+
end
|
110
|
+
true
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def get(key, validated) # :nodoc
|
116
|
+
if @config.has_key?(@environment) &&
|
117
|
+
@config[@environment].has_key?(key)
|
118
|
+
return @config[@environment][key]
|
119
|
+
else
|
120
|
+
@config['common'][key]
|
121
|
+
end
|
122
|
+
rescue NoMethodError
|
123
|
+
raise MissingConfiguration, "Configuration key '#{key}' not found"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module GlobalSession
|
2
|
+
# The global session directory, which provides some lookup and decision services
|
3
|
+
# to instances of Session.
|
4
|
+
#
|
5
|
+
# The default implementation is simplistic, but should be suitable for most applications.
|
6
|
+
# Directory is designed to be specialized via subclassing. To override the behavior to
|
7
|
+
# suit your needs, simply create a subclass of Directory and add a configuration file
|
8
|
+
# setting to specify the class name of your implementation:
|
9
|
+
#
|
10
|
+
# common:
|
11
|
+
# directory: MyCoolDirectory
|
12
|
+
#
|
13
|
+
#
|
14
|
+
# === The Authority Keystore
|
15
|
+
# Directory uses a filesystem directory as a backing store for RSA
|
16
|
+
# public keys of global session authorities. The directory should
|
17
|
+
# contain one or more +*.pub+ files containing OpenSSH-format public
|
18
|
+
# RSA keys. The name of the pub file determines the name of the
|
19
|
+
# authority it represents.
|
20
|
+
#
|
21
|
+
# === The Local Authority
|
22
|
+
# Directory will infer the name of the local authority (if any) by
|
23
|
+
# looking for a private-key file in the keystore. If a +*.key+ file
|
24
|
+
# is found, then its name is taken to be the name of the local
|
25
|
+
# authority and all GlobalSessions created will be signed by that
|
26
|
+
# authority's private key.
|
27
|
+
#
|
28
|
+
# If more than one key file is found, Directory will raise an error
|
29
|
+
# at initialization time.
|
30
|
+
#
|
31
|
+
class Directory
|
32
|
+
attr_reader :configuration, :authorities, :private_key, :local_authority_name
|
33
|
+
|
34
|
+
# Create a new Directory.
|
35
|
+
#
|
36
|
+
# === Parameters
|
37
|
+
# keystore_directory(String):: Absolute path to authority keystore
|
38
|
+
#
|
39
|
+
# ===Raise
|
40
|
+
# ConfigurationError:: if too many or too few keys are found, or if *.key/*.pub files are malformatted
|
41
|
+
def initialize(configuration, keystore_directory)
|
42
|
+
@configuration = configuration
|
43
|
+
certs = Dir[File.join(keystore_directory, '*.pub')]
|
44
|
+
keys = Dir[File.join(keystore_directory, '*.key')]
|
45
|
+
raise ConfigurationError, "Excepted 0 or 1 key files, found #{keys.size}" unless [0, 1].include?(keys.size)
|
46
|
+
|
47
|
+
@authorities = {}
|
48
|
+
certs.each do |cert_file|
|
49
|
+
basename = File.basename(cert_file)
|
50
|
+
authority = basename[0...(basename.rindex('.'))] #chop trailing .ext
|
51
|
+
@authorities[authority] = OpenSSL::PKey::RSA.new(File.read(cert_file))
|
52
|
+
raise ConfigurationError, "Expected #{basename} to contain an RSA public key" unless @authorities[authority].public?
|
53
|
+
end
|
54
|
+
|
55
|
+
if (authority_name = @configuration['authority'])
|
56
|
+
key_file = keys.detect { |kf| kf =~ /#{authority_name}.key$/ }
|
57
|
+
raise ConfigurationError, "Key file #{authority_name}.key not found" unless key_file
|
58
|
+
@private_key = OpenSSL::PKey::RSA.new(File.read(key_file))
|
59
|
+
raise ConfigurationError, "Expected #{key_file} to contain an RSA private key" unless @private_key.private?
|
60
|
+
@local_authority_name = authority_name
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Determine whether this system trusts a particular authority based on
|
65
|
+
# the trust settings specified in Configuration.
|
66
|
+
#
|
67
|
+
# === Parameters
|
68
|
+
# authority(String):: The name of the authority
|
69
|
+
#
|
70
|
+
# === Return
|
71
|
+
# trusted(true|false):: whether the local system trusts sessions signed by the specified authority
|
72
|
+
def trusted_authority?(authority)
|
73
|
+
@configuration['trust'].include?(authority)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Determine whether the given session UUID is valid. The default implementation only considers
|
77
|
+
# a session to be invalid if its expired_at timestamp is in the past. Custom implementations
|
78
|
+
# might want to consider other factors, such as whether the user has signed out of this node
|
79
|
+
# or another node (perhaps using some sort of centralized lookup or single sign-out mechanism).
|
80
|
+
#
|
81
|
+
# === Parameters
|
82
|
+
# uuid(String):: Global session UUID
|
83
|
+
# expired_at(Time):: When the session expired (or will expire)
|
84
|
+
#
|
85
|
+
# === Return
|
86
|
+
# valid(true|false):: whether the specified session is valid
|
87
|
+
def valid_session?(uuid, expired_at)
|
88
|
+
expired_at > Time.now
|
89
|
+
end
|
90
|
+
|
91
|
+
# Callback used by Session objects to report when the application code calls
|
92
|
+
# #invalidate! on them. The default implementation of this method does nothing.
|
93
|
+
#
|
94
|
+
# uuid(String):: Global session UUID
|
95
|
+
# expired_at(Time):: When the session expired
|
96
|
+
#
|
97
|
+
# === Return
|
98
|
+
# true:: Always returns true
|
99
|
+
def report_invalid_session(uuid, expired_at)
|
100
|
+
true
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module GlobalSession
|
2
|
+
# Various encoding (not encryption!) techniques used by the global session plugin.
|
3
|
+
#
|
4
|
+
module Encoding
|
5
|
+
# JSON serializer, used to serialize Hash objects in a form suitable
|
6
|
+
# for stuffing into a cookie.
|
7
|
+
#
|
8
|
+
class JSON
|
9
|
+
# Unserialize JSON to Hash.
|
10
|
+
#
|
11
|
+
# === Parameters
|
12
|
+
# json(String):: A well-formed JSON document
|
13
|
+
#
|
14
|
+
# === Return
|
15
|
+
# value(Hash):: An unserialized Ruby Hash
|
16
|
+
def self.load(json)
|
17
|
+
::JSON.load(json)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Serialize Hash to JSON document.
|
21
|
+
#
|
22
|
+
# === Parameters
|
23
|
+
# value(Hash):: The hash to be serialized
|
24
|
+
#
|
25
|
+
# === Return
|
26
|
+
# json(String):: A JSON-serialized representation of +value+
|
27
|
+
def self.dump(object)
|
28
|
+
return object.to_json
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Implements URL encoding, but without newlines, and using '-' and '_' as
|
33
|
+
# the 62nd and 63rd symbol instead of '+' and '/'. This makes for encoded
|
34
|
+
# values that can be easily stored in a cookie; however, they cannot
|
35
|
+
# be used in a URL query string without URL-escaping them since they
|
36
|
+
# will contain '=' characters.
|
37
|
+
#
|
38
|
+
# This scheme is almost identical to the scheme "Base 64 Encoding with URL
|
39
|
+
# and Filename Safe Alphabet," described in RFC4648, with the exception that
|
40
|
+
# this scheme preserves the '=' padding characters due to limitations of
|
41
|
+
# Ruby's built-in base64 encoding routines.
|
42
|
+
class Base64Cookie
|
43
|
+
# Decode a B64cookie-encoded string.
|
44
|
+
#
|
45
|
+
# === Parameters
|
46
|
+
# encoded(String):: The encoded string
|
47
|
+
#
|
48
|
+
# === Return
|
49
|
+
# decoded(String):: The decoded result, which may contain nonprintable bytes
|
50
|
+
def self.load(string)
|
51
|
+
tr = string.tr('-_', '+/')
|
52
|
+
return tr.unpack('m')[0]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Encode a Ruby (ASCII or binary) string.
|
56
|
+
#
|
57
|
+
# === Parameters
|
58
|
+
# decoded(String):: The raw string to be encoded
|
59
|
+
#
|
60
|
+
# === Return
|
61
|
+
# encoded(String):: The B64cookie-encoded result.
|
62
|
+
def self.dump(object)
|
63
|
+
raw = [object].pack('m')
|
64
|
+
raw.tr!('+/', '-_')
|
65
|
+
raw.gsub!("\n", '')
|
66
|
+
return raw
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module GlobalSession
|
2
|
+
# Helper class that enables the end user to treat the global and local session as if
|
3
|
+
# they were the same object. This is accomplished by implementing approximately the
|
4
|
+
# same interface as a Hash, and dispatching to one or the other session object depending
|
5
|
+
# on various factors.
|
6
|
+
#
|
7
|
+
# This class isn't intended to be used directly by the end user. Instead, set integrated: true
|
8
|
+
# in the configuration file and the Web framework integration code will manage an integrated
|
9
|
+
# session object for you, as well as overriding the framework's default session accessor to
|
10
|
+
# return an integrated session instead.
|
11
|
+
#
|
12
|
+
# When using an integrated session, you can always get to the underlying objects by
|
13
|
+
# using the #local and #global readers of this class.
|
14
|
+
#
|
15
|
+
class IntegratedSession
|
16
|
+
# Return the local-session objects, whose type may vary depending on the Web framework.
|
17
|
+
attr_reader :local
|
18
|
+
|
19
|
+
# Return the global-session object.
|
20
|
+
attr_reader :global
|
21
|
+
|
22
|
+
# Construct a new integrated session.
|
23
|
+
#
|
24
|
+
# === Parameters
|
25
|
+
# local(Object):: Local session that acts like a Hash
|
26
|
+
# global(Session):: Session
|
27
|
+
def initialize(local, global)
|
28
|
+
@local = local
|
29
|
+
@global = global
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve a value from the global session if the supplied key is supported by
|
33
|
+
# the global session, else retrieve it from the local session.
|
34
|
+
#
|
35
|
+
# === Parameters
|
36
|
+
# key(String):: the key
|
37
|
+
#
|
38
|
+
# === Return
|
39
|
+
# value(Object):: The value associated with +key+, or nil if +key+ is not present
|
40
|
+
def [](key)
|
41
|
+
key = key.to_s
|
42
|
+
if @global.supports_key?(key)
|
43
|
+
@global[key]
|
44
|
+
else
|
45
|
+
@local[key]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Set a value in the global session (if the supplied key is supported) or the local
|
50
|
+
# session otherwise.
|
51
|
+
#
|
52
|
+
# === Parameters
|
53
|
+
# key(String):: The key to set
|
54
|
+
# value(Object):: The value to set
|
55
|
+
#
|
56
|
+
# === Return
|
57
|
+
# value(Object):: Always returns the value that was set
|
58
|
+
def []=(key, value)
|
59
|
+
key = key.to_s
|
60
|
+
if @global.supports_key?(key)
|
61
|
+
@global[key] = value
|
62
|
+
else
|
63
|
+
@local[key] = value
|
64
|
+
end
|
65
|
+
|
66
|
+
return value
|
67
|
+
end
|
68
|
+
|
69
|
+
# Determine whether the global or local session contains a value with the specified key.
|
70
|
+
#
|
71
|
+
# === Parameters
|
72
|
+
# key(String):: The name of the key
|
73
|
+
#
|
74
|
+
# === Return
|
75
|
+
# contained(true|false):: Whether the session currently has a value for the specified key.
|
76
|
+
def has_key?(key)
|
77
|
+
key = key.to_s
|
78
|
+
@global.has_key?(key) || @local.has_key?(key)
|
79
|
+
end
|
80
|
+
|
81
|
+
# Return the keys that are currently present in either the global or local session.
|
82
|
+
#
|
83
|
+
# === Return
|
84
|
+
# keys(Array):: List of keys contained in the global or local session.
|
85
|
+
def keys
|
86
|
+
@global.keys + @local.keys
|
87
|
+
end
|
88
|
+
|
89
|
+
# Return the values that are currently present in the global or local session.
|
90
|
+
#
|
91
|
+
# === Return
|
92
|
+
# values(Array):: List of values contained in the global or local session.
|
93
|
+
def values
|
94
|
+
@global.values + @local.values
|
95
|
+
end
|
96
|
+
|
97
|
+
# Iterate over each key/value pair in both the global and local session.
|
98
|
+
#
|
99
|
+
# === Block
|
100
|
+
# An iterator which will be called with each key/value pair
|
101
|
+
#
|
102
|
+
# === Return
|
103
|
+
# Returns the value of the last expression evaluated by the block
|
104
|
+
def each_pair(&block)
|
105
|
+
@global.each_pair(&block)
|
106
|
+
@local.each_pair(&block)
|
107
|
+
end
|
108
|
+
|
109
|
+
def respond_to?(meth) # :nodoc:
|
110
|
+
return @global.respond_to?(meth) || @local.respond_to?(meth) || super
|
111
|
+
end
|
112
|
+
|
113
|
+
def method_missing(meth, *args) # :nodoc:
|
114
|
+
if @global.respond_to?(meth)
|
115
|
+
@global.__send__(meth, *args)
|
116
|
+
elsif @local.respond_to?(meth)
|
117
|
+
@local.__send__(meth, *args)
|
118
|
+
else
|
119
|
+
super
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|