couchrest_session_store 0.1.3 → 0.2.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/.travis.yml +4 -0
- data/Gemfile +7 -0
- data/README.md +5 -1
- data/couchrest_session_store.gemspec +25 -0
- data/lib/couchrest/session/document.rb +76 -0
- data/lib/couchrest/session/store.rb +75 -0
- data/lib/couchrest/session/utility.rb +16 -0
- data/lib/couchrest/session.rb +5 -0
- data/lib/couchrest_session_store.rb +3 -114
- data/test/couch_tester.rb +26 -0
- data/test/session_store_test.rb +111 -0
- data/test/test_clock.rb +12 -0
- data/test/test_helper.rb +6 -0
- metadata +13 -2
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/README.md
CHANGED
@@ -1,7 +1,11 @@
|
|
1
|
-
# CouchRest
|
1
|
+
# CouchRest::Session::Store #
|
2
2
|
|
3
3
|
A simple session store based on CouchRest Model.
|
4
4
|
|
5
|
+
It will automatically pick up the config/couch.yml file for CouchRest Model
|
6
|
+
|
5
7
|
## Options ##
|
6
8
|
|
7
9
|
* marhal_data: (_defaults true_) - if set to false session data will be stored directly in the couch document. Otherwise it's marshalled and base64 encoded to enable restoring ruby data structures.
|
10
|
+
* database: database to use combined with config prefix and suffix
|
11
|
+
* exprire_after: livetime of a session in seconds
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# _*_ encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
|
5
|
+
gem.authors = ["Azul"]
|
6
|
+
gem.email = ["azul@leap.se"]
|
7
|
+
gem.summary = "A Rails Session Store based on CouchRest Model"
|
8
|
+
gem.description = gem.summary
|
9
|
+
gem.homepage = "http://github.com/azul/couchrest_session_store"
|
10
|
+
|
11
|
+
gem.has_rdoc = true
|
12
|
+
# gem.extra_rdoc_files = ["LICENSE"]
|
13
|
+
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.name = "couchrest_session_store"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = '0.2.0'
|
18
|
+
|
19
|
+
gem.add_dependency "couchrest"
|
20
|
+
gem.add_dependency "couchrest_model"
|
21
|
+
gem.add_dependency "actionpack"
|
22
|
+
|
23
|
+
gem.add_development_dependency "minitest"
|
24
|
+
gem.add_development_dependency "rake"
|
25
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'couchrest/session/utility'
|
2
|
+
|
3
|
+
class CouchRest::Session::Document
|
4
|
+
include CouchRest::Session::Utility
|
5
|
+
|
6
|
+
def initialize(doc)
|
7
|
+
@doc = doc
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.load(sid)
|
11
|
+
self.allocate.tap do |session_doc|
|
12
|
+
session_doc.load(sid)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.build(sid, session, options)
|
17
|
+
self.new(CouchRest::Document.new({"_id" => sid})).tap do |session_doc|
|
18
|
+
session_doc.update session, options
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def load(sid)
|
23
|
+
@doc = database.get(sid)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_session
|
27
|
+
if doc["marshalled"]
|
28
|
+
session = unmarshal(doc["data"])
|
29
|
+
else
|
30
|
+
session = doc["data"]
|
31
|
+
end
|
32
|
+
return session
|
33
|
+
end
|
34
|
+
|
35
|
+
def delete
|
36
|
+
database.delete_doc(doc)
|
37
|
+
end
|
38
|
+
|
39
|
+
def update(session, options)
|
40
|
+
# clean up old data but leave id and revision intact
|
41
|
+
doc.reject! do |k,v|
|
42
|
+
k[0] != '_'
|
43
|
+
end
|
44
|
+
doc.merge! data_for_doc(session, options)
|
45
|
+
end
|
46
|
+
|
47
|
+
def save
|
48
|
+
database.save_doc(doc)
|
49
|
+
end
|
50
|
+
|
51
|
+
def expired?
|
52
|
+
expires && expires < Time.now
|
53
|
+
end
|
54
|
+
|
55
|
+
protected
|
56
|
+
|
57
|
+
def data_for_doc(session, options)
|
58
|
+
{ "data" => options[:marshal_data] ? marshal(session) : session,
|
59
|
+
"marshalled" => options[:marshal_data],
|
60
|
+
"expires" => expiry_from_options(options) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def expiry_from_options(options)
|
64
|
+
expire_after = options[:expire_after]
|
65
|
+
expire_after && (Time.now + expire_after)
|
66
|
+
end
|
67
|
+
|
68
|
+
def expires
|
69
|
+
doc["expires"] && Time.parse_iso8601(doc["expires"])
|
70
|
+
end
|
71
|
+
|
72
|
+
def doc
|
73
|
+
@doc
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class CouchRest::Session::Store < ActionDispatch::Session::AbstractStore
|
2
|
+
|
3
|
+
include CouchRest::Model::Configuration
|
4
|
+
include CouchRest::Model::Connection
|
5
|
+
|
6
|
+
def initialize(app, options = {})
|
7
|
+
super
|
8
|
+
self.class.set_options(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.set_options(options)
|
12
|
+
@options = options
|
13
|
+
end
|
14
|
+
|
15
|
+
# just fetch from the config
|
16
|
+
def self.database
|
17
|
+
@database ||= initialize_database
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.initialize_database
|
21
|
+
use_database @options[:database] || "sessions"
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def get_session(env, sid)
|
27
|
+
if session = fetch_session(sid)
|
28
|
+
[sid, session]
|
29
|
+
else
|
30
|
+
[generate_sid, {}]
|
31
|
+
end
|
32
|
+
rescue RestClient::ResourceNotFound
|
33
|
+
# session data does not exist anymore
|
34
|
+
return [sid, {}]
|
35
|
+
end
|
36
|
+
|
37
|
+
def set_session(env, sid, session, options)
|
38
|
+
doc = build_or_update_doc(sid, session, options)
|
39
|
+
doc.save
|
40
|
+
return sid
|
41
|
+
end
|
42
|
+
|
43
|
+
def destroy_session(env, sid, options)
|
44
|
+
doc = secure_get(sid)
|
45
|
+
doc.delete
|
46
|
+
generate_sid unless options[:drop]
|
47
|
+
rescue RestClient::ResourceNotFound
|
48
|
+
# already destroyed - we're done.
|
49
|
+
generate_sid unless options[:drop]
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch_session(sid)
|
53
|
+
return nil unless sid
|
54
|
+
doc = secure_get(sid)
|
55
|
+
doc.to_session unless doc.expired?
|
56
|
+
end
|
57
|
+
|
58
|
+
def build_or_update_doc(sid, session, options)
|
59
|
+
options[:marshal_data] = true if options[:marshal_data].nil?
|
60
|
+
doc = secure_get(sid)
|
61
|
+
doc.update(session, options)
|
62
|
+
return doc
|
63
|
+
rescue RestClient::ResourceNotFound
|
64
|
+
CouchRest::Session::Document.build(sid, session, options)
|
65
|
+
end
|
66
|
+
|
67
|
+
# prevent access to design docs
|
68
|
+
# this should be prevented on a couch permission level as well.
|
69
|
+
# but better be save than sorry.
|
70
|
+
def secure_get(sid)
|
71
|
+
raise RestClient::ResourceNotFound if /^_design\/(.*)/ =~ sid
|
72
|
+
CouchRest::Session::Document.load(sid)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module CouchRest::Session::Utility
|
2
|
+
module_function
|
3
|
+
|
4
|
+
def marshal(data)
|
5
|
+
::Base64.encode64(Marshal.dump(data)) if data
|
6
|
+
end
|
7
|
+
|
8
|
+
def unmarshal(data)
|
9
|
+
Marshal.load(::Base64.decode64(data)) if data
|
10
|
+
end
|
11
|
+
|
12
|
+
def database
|
13
|
+
CouchRest::Session::Store.database
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -4,118 +4,7 @@ require 'couchrest_model'
|
|
4
4
|
gem 'actionpack', '~> 3.0'
|
5
5
|
require 'action_dispatch'
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
#
|
11
|
-
# Options:
|
12
|
-
# :database => database to use combined with config prefix and suffix
|
13
|
-
#
|
14
|
-
class CouchRestSessionStore < ActionDispatch::Session::AbstractStore
|
15
|
-
|
16
|
-
include CouchRest::Model::Configuration
|
17
|
-
include CouchRest::Model::Connection
|
18
|
-
|
19
|
-
class << self
|
20
|
-
def marshal(data)
|
21
|
-
::Base64.encode64(Marshal.dump(data)) if data
|
22
|
-
end
|
23
|
-
|
24
|
-
def unmarshal(data)
|
25
|
-
Marshal.load(::Base64.decode64(data)) if data
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|
29
|
-
|
30
|
-
def initialize(app, options = {})
|
31
|
-
super
|
32
|
-
self.class.set_options(options)
|
33
|
-
end
|
34
|
-
|
35
|
-
def self.set_options(options)
|
36
|
-
@options = options
|
37
|
-
end
|
38
|
-
|
39
|
-
# just fetch from the config
|
40
|
-
def self.database
|
41
|
-
@database ||= initialize_database
|
42
|
-
end
|
43
|
-
|
44
|
-
def self.initialize_database
|
45
|
-
use_database @options[:database] || "sessions"
|
46
|
-
end
|
47
|
-
|
48
|
-
def database
|
49
|
-
self.class.database
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
|
54
|
-
def get_session(env, sid)
|
55
|
-
if sid
|
56
|
-
doc = secure_get(sid)
|
57
|
-
if doc["not_marshalled"]
|
58
|
-
session = doc.to_hash
|
59
|
-
session.delete("not_marshalled")
|
60
|
-
else
|
61
|
-
session = self.class.unmarshal(doc["data"])
|
62
|
-
end
|
63
|
-
[sid, session]
|
64
|
-
else
|
65
|
-
[generate_sid, {}]
|
66
|
-
end
|
67
|
-
rescue RestClient::ResourceNotFound
|
68
|
-
# session data does not exist anymore
|
69
|
-
return [sid, {}]
|
70
|
-
end
|
71
|
-
|
72
|
-
def set_session(env, sid, session, options)
|
73
|
-
doc = build_or_update_doc(sid, session, options[:marshal_data])
|
74
|
-
database.save_doc(doc)
|
75
|
-
return sid
|
76
|
-
end
|
77
|
-
|
78
|
-
def destroy_session(env, sid, options)
|
79
|
-
doc = secure_get(sid)
|
80
|
-
database.delete_doc(doc)
|
81
|
-
generate_sid unless options[:drop]
|
82
|
-
rescue RestClient::ResourceNotFound
|
83
|
-
# already destroyed - we're done.
|
84
|
-
generate_sid unless options[:drop]
|
85
|
-
end
|
86
|
-
|
87
|
-
def build_or_update_doc(sid, session, marshal_data)
|
88
|
-
marshal_data = true if marshal_data.nil?
|
89
|
-
doc = secure_get(sid)
|
90
|
-
update_doc doc, data_for_doc(session, marshal_data)
|
91
|
-
return doc
|
92
|
-
rescue RestClient::ResourceNotFound
|
93
|
-
data = data_for_doc(session, marshal_data).merge({"_id" => sid})
|
94
|
-
return CouchRest::Document.new(data)
|
95
|
-
end
|
96
|
-
|
97
|
-
def update_doc(doc, data)
|
98
|
-
# clean up old data but leave id and revision intact
|
99
|
-
doc.reject! do |k,v|
|
100
|
-
k[0] != '_'
|
101
|
-
end
|
102
|
-
doc.merge! data
|
103
|
-
end
|
104
|
-
|
105
|
-
def data_for_doc(session, marshal_data)
|
106
|
-
if marshal_data
|
107
|
-
{ "data" => self.class.marshal(session) }
|
108
|
-
else
|
109
|
-
session.merge({"not_marshalled" => true})
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
|
-
# prevent access to design docs
|
114
|
-
# this should be prevented on a couch permission level as well.
|
115
|
-
# but better be save than sorry.
|
116
|
-
def secure_get(sid)
|
117
|
-
raise RestClient::ResourceNotFound if /^_design\/(.*)/ =~ sid
|
118
|
-
database.get(sid)
|
119
|
-
end
|
120
|
-
end
|
7
|
+
require 'couchrest/session'
|
8
|
+
require 'couchrest/session/store'
|
9
|
+
require 'couchrest/session/document'
|
121
10
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
#
|
2
|
+
# Access the couch directly so we can test its state without relying
|
3
|
+
# on the SessionStore
|
4
|
+
#
|
5
|
+
|
6
|
+
class CouchTester
|
7
|
+
include CouchRest::Model::Configuration
|
8
|
+
include CouchRest::Model::Connection
|
9
|
+
|
10
|
+
attr_reader :database
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
@database = self.class.use_database options[:database] || "sessions"
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(sid)
|
17
|
+
database.get(sid)
|
18
|
+
end
|
19
|
+
|
20
|
+
def update(sid, diff)
|
21
|
+
doc = database.get(sid)
|
22
|
+
doc.merge! diff
|
23
|
+
database.save_doc(doc)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class SessionStoreTest < MiniTest::Test
|
4
|
+
|
5
|
+
def test_session_initialization
|
6
|
+
sid, session = store.send :get_session, env, nil
|
7
|
+
assert sid
|
8
|
+
assert_equal Hash.new, session
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_normal_session_flow
|
12
|
+
sid, session = init_session
|
13
|
+
store_session(sid, session)
|
14
|
+
assert_equal [sid, session], store.send(:get_session, env, sid)
|
15
|
+
store.send :destroy_session, env, sid, {}
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_updating_session
|
19
|
+
sid, session = init_session
|
20
|
+
store_session(sid, session)
|
21
|
+
session[:bla] = "blub"
|
22
|
+
store.send :set_session, env, sid, session, {}
|
23
|
+
assert_equal [sid, session], store.send(:get_session, env, sid)
|
24
|
+
store.send :destroy_session, env, sid, {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_unmarshalled_session_flow
|
28
|
+
sid, session = init_session
|
29
|
+
store_session sid, session, :marshal_data => false
|
30
|
+
new_sid, new_session = store.send(:get_session, env, sid)
|
31
|
+
assert_equal sid, new_sid
|
32
|
+
assert_equal session[:key], new_session["key"]
|
33
|
+
store.send :destroy_session, env, sid, {}
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_unmarshalled_data
|
37
|
+
sid, session = init_session
|
38
|
+
store_session sid, session, :marshal_data => false
|
39
|
+
couch = CouchTester.new
|
40
|
+
data = couch.get(sid)["data"]
|
41
|
+
assert_equal session[:key], data["key"]
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_logout_in_between
|
45
|
+
sid, session = init_session
|
46
|
+
store_session(sid, session)
|
47
|
+
store.send :destroy_session, env, sid, {}
|
48
|
+
other_sid, other_session = store.send(:get_session, env, sid)
|
49
|
+
assert_equal Hash.new, other_session
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_can_logout_twice
|
53
|
+
sid, session = init_session
|
54
|
+
store_session(sid, session)
|
55
|
+
store.send :destroy_session, env, sid, {}
|
56
|
+
store.send :destroy_session, env, sid, {}
|
57
|
+
other_sid, other_session = store.send(:get_session, env, sid)
|
58
|
+
assert_equal Hash.new, other_session
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_stored_and_not_expired_yet
|
62
|
+
sid, session = init_session
|
63
|
+
store_session(sid, session, expire_after: 300)
|
64
|
+
doc = CouchRest::Session::Document.load(sid)
|
65
|
+
expires = doc.send :expires
|
66
|
+
assert expires
|
67
|
+
assert !doc.expired?
|
68
|
+
assert (expires - Time.now) > 0, "Exiry should be in the future"
|
69
|
+
assert (expires - Time.now) <= 300, "Should expire after 300 seconds - not more"
|
70
|
+
assert_equal [sid, session], store.send(:get_session, env, sid)
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_stored_but_expired
|
74
|
+
sid, session = init_session
|
75
|
+
store_session(sid, session, expire_after: 300)
|
76
|
+
CouchTester.new.update(sid, "expires" => Time.now - 2.minutes)
|
77
|
+
other_sid, other_session = store.send(:get_session, env, sid)
|
78
|
+
assert_equal Hash.new, other_session, "session should have expired"
|
79
|
+
assert other_sid != sid
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_store_without_expiry
|
83
|
+
sid, session = init_session
|
84
|
+
store_session(sid, session)
|
85
|
+
couch = CouchTester.new
|
86
|
+
assert_nil couch.get(sid)["expires"]
|
87
|
+
assert_equal [sid, session], store.send(:get_session, env, sid)
|
88
|
+
end
|
89
|
+
|
90
|
+
def app
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def store(options = {})
|
95
|
+
@store ||= CouchRest::Session::Store.new(app, options)
|
96
|
+
end
|
97
|
+
|
98
|
+
def env(settings = {})
|
99
|
+
env ||= settings
|
100
|
+
end
|
101
|
+
|
102
|
+
def init_session
|
103
|
+
sid, session = store.send :get_session, env, nil
|
104
|
+
session[:key] = "stub"
|
105
|
+
return sid, session
|
106
|
+
end
|
107
|
+
|
108
|
+
def store_session(sid, session, options = {})
|
109
|
+
store.send :set_session, env, sid, session, options
|
110
|
+
end
|
111
|
+
end
|
data/test/test_clock.rb
ADDED
data/test/test_helper.rb
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
gem 'minitest'
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/../lib/couchrest_session_store.rb')
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/couch_tester.rb')
|
6
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_clock.rb')
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: couchrest_session_store
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-09-17 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: couchrest
|
@@ -98,9 +98,20 @@ executables: []
|
|
98
98
|
extensions: []
|
99
99
|
extra_rdoc_files: []
|
100
100
|
files:
|
101
|
+
- .travis.yml
|
102
|
+
- Gemfile
|
101
103
|
- README.md
|
102
104
|
- Rakefile
|
105
|
+
- couchrest_session_store.gemspec
|
106
|
+
- lib/couchrest/session.rb
|
107
|
+
- lib/couchrest/session/document.rb
|
108
|
+
- lib/couchrest/session/store.rb
|
109
|
+
- lib/couchrest/session/utility.rb
|
103
110
|
- lib/couchrest_session_store.rb
|
111
|
+
- test/couch_tester.rb
|
112
|
+
- test/session_store_test.rb
|
113
|
+
- test/test_clock.rb
|
114
|
+
- test/test_helper.rb
|
104
115
|
homepage: http://github.com/azul/couchrest_session_store
|
105
116
|
licenses: []
|
106
117
|
post_install_message:
|