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