ey_gatekeeper 0.1.34
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/ey_gatekeeper/access_control_list.rb +133 -0
- data/lib/ey_gatekeeper/client/consumer.rb +63 -0
- data/lib/ey_gatekeeper/client/middlewares/authentication.rb +77 -0
- data/lib/ey_gatekeeper/client/middlewares/service_token_authentication.rb +39 -0
- data/lib/ey_gatekeeper/client/server_response.rb +37 -0
- data/lib/ey_gatekeeper/client.rb +44 -0
- data/lib/ey_gatekeeper/responses.rb +67 -0
- data/lib/ey_gatekeeper/token.rb +29 -0
- data/lib/ey_gatekeeper/util.rb +23 -0
- data/lib/ey_gatekeeper/version.rb +5 -0
- data/lib/ey_gatekeeper.rb +54 -0
- data/spec/access_control_list_spec.rb +192 -0
- data/spec/auth_service_spec.rb +332 -0
- data/spec/consumer_spec.rb +80 -0
- data/spec/fallback_spec.rb +76 -0
- data/spec/impersonation_spec.rb +50 -0
- data/spec/responses_spec.rb +23 -0
- data/spec/service_token_authentication_spec.rb +10 -0
- data/spec/spec_helper.rb +133 -0
- metadata +129 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'cgi'
|
3
|
+
|
4
|
+
module EY
|
5
|
+
module GateKeeper
|
6
|
+
|
7
|
+
# An individual control, probably within a list.
|
8
|
+
# Example: AccessControl.new('xdna://foobars', ['GET', 'PUT', 'DELETE'])
|
9
|
+
class AccessControl
|
10
|
+
attr_reader :path, :methods
|
11
|
+
|
12
|
+
def initialize(path, *methods)
|
13
|
+
@path, @methods = path, methods.flatten.map {|m| m.to_s.upcase }
|
14
|
+
end
|
15
|
+
|
16
|
+
def allow?(method)
|
17
|
+
methods.include?(method.to_s.upcase)
|
18
|
+
end
|
19
|
+
|
20
|
+
def suitable_for?(test_path)
|
21
|
+
test_path = URI.escape(URI.unescape(test_path))
|
22
|
+
test_path_uri = URI.parse(test_path)
|
23
|
+
|
24
|
+
matches?(test_path_uri) && parameters_satisfied?(test_path_uri)
|
25
|
+
end
|
26
|
+
|
27
|
+
def matches?(test_path_uri)
|
28
|
+
path_without_parameters == 'xdna://' ||
|
29
|
+
"xdna:/#{test_path_uri.path}".match(%r{^#{path_without_parameters}(\/|$)})
|
30
|
+
end
|
31
|
+
|
32
|
+
def parameters_satisfied?(test_path_uri)
|
33
|
+
(required_parameters - CGI.parse(test_path_uri.query || '').keys).empty?
|
34
|
+
end
|
35
|
+
|
36
|
+
def required_parameters
|
37
|
+
@required_parameters ||= CGI.parse(query || '').keys
|
38
|
+
end
|
39
|
+
|
40
|
+
# can't use URI for these right now since it barfs on _ in the
|
41
|
+
# hostname portion
|
42
|
+
def path_without_parameters
|
43
|
+
path.split('?').first
|
44
|
+
end
|
45
|
+
|
46
|
+
def query
|
47
|
+
path.split('?', 2)[1]
|
48
|
+
end
|
49
|
+
|
50
|
+
# Intersect 2 acccess controls
|
51
|
+
def &(other_control)
|
52
|
+
AccessControl.new(path, other_control.methods & methods)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# A list of AccessControl objects. Can check a request method and path against all controls in the list.
|
57
|
+
# Example: AccessControlList.new({ 'xdna://foobars' => ['GET', 'PUT', 'DELETE'], 'xdna://foobazes' => [] })
|
58
|
+
class AccessControlList
|
59
|
+
attr_reader :list
|
60
|
+
|
61
|
+
def initialize(list)
|
62
|
+
@list = case list
|
63
|
+
when Hash
|
64
|
+
list.map {|path, methods| AccessControl.new(path, methods) }
|
65
|
+
when Array
|
66
|
+
list
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def allow?(method, path)
|
71
|
+
control = control_for(path)
|
72
|
+
control && control.allow?(method.to_s.upcase)
|
73
|
+
end
|
74
|
+
|
75
|
+
def deny?(method, path)
|
76
|
+
!allow?(method, path)
|
77
|
+
end
|
78
|
+
|
79
|
+
def control_for(path)
|
80
|
+
matches = list.find_all {|c| c.suitable_for?(path) }
|
81
|
+
matches.sort_by {|c| c.path.size }.last
|
82
|
+
end
|
83
|
+
|
84
|
+
def paths
|
85
|
+
list.map {|c| c.path }
|
86
|
+
end
|
87
|
+
|
88
|
+
def [](path)
|
89
|
+
list.detect {|c| c.path == path }
|
90
|
+
end
|
91
|
+
|
92
|
+
# Intersect the two lists
|
93
|
+
def &(other_list)
|
94
|
+
new_list = (paths + other_list.paths).uniq.map do |path|
|
95
|
+
if self[path] && other_list[path]
|
96
|
+
# If both lists have the path, do a simple control AND
|
97
|
+
self[path] & other_list[path]
|
98
|
+
elsif self[path]
|
99
|
+
# If only one side has the path, see if there's a less specific control we can AND with
|
100
|
+
pare_down(self[path], other_list)
|
101
|
+
elsif other_list[path]
|
102
|
+
# Same for here, just opposite
|
103
|
+
pare_down(other_list[path], self)
|
104
|
+
end
|
105
|
+
end.compact
|
106
|
+
|
107
|
+
AccessControlList.new(new_list)
|
108
|
+
end
|
109
|
+
|
110
|
+
def to_hash
|
111
|
+
{}.tap do |result|
|
112
|
+
list.each do |control|
|
113
|
+
result.update(control.path => control.methods)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
def pare_down(target, possibilities)
|
121
|
+
# Find a less specific control with the longest path
|
122
|
+
# starting with target.path. We can then use that control for
|
123
|
+
# performing an AND. This covers the xdna:// vs. xdna://foos
|
124
|
+
# case.
|
125
|
+
most_specific_less_specific_control = possibilities.list.
|
126
|
+
find_all {|p| p.path == 'xdna://' || target.path =~ %r{^#{p.path}(\/|$)} }.sort_by {|p| p.path.size }.last
|
127
|
+
return unless most_specific_less_specific_control
|
128
|
+
|
129
|
+
target & most_specific_less_specific_control
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Client
|
4
|
+
class Consumer < Rack::Client::Simple
|
5
|
+
attr_accessor :token, :key, :secret, :impersonation_token
|
6
|
+
|
7
|
+
def initialize(key, secret, options = {})
|
8
|
+
@key = key
|
9
|
+
@secret = secret
|
10
|
+
@endpoint = options[:endpoint]
|
11
|
+
@token = nil
|
12
|
+
|
13
|
+
if @endpoint
|
14
|
+
super(build_app(options), @endpoint)
|
15
|
+
else
|
16
|
+
super(build_app(options))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def token
|
21
|
+
get('/cloudkit-meta')
|
22
|
+
@token
|
23
|
+
end
|
24
|
+
|
25
|
+
def impersonate!(impersonation_token)
|
26
|
+
@impersonation_token = case impersonation_token
|
27
|
+
when EY::GateKeeper::Token
|
28
|
+
impersonation_token.token
|
29
|
+
when String, NilClass
|
30
|
+
impersonation_token
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def just_me!
|
35
|
+
impersonate!(nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
def effective_access_controls
|
39
|
+
response = get('/_effective_access_controls')
|
40
|
+
if response.status == 200 && response.content_type == 'application/json'
|
41
|
+
JSON.parse(response.body) # rack-client collapses the body for us
|
42
|
+
else
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def build_app(options)
|
48
|
+
inner_app = options.fetch(:app, Rack::Client::Handler::NetHTTP.new)
|
49
|
+
token_setter = method(:token=)
|
50
|
+
key_method = method(:key)
|
51
|
+
secret_method = method(:secret)
|
52
|
+
impersonation_token_method = method(:impersonation_token)
|
53
|
+
|
54
|
+
Rack::Builder.app do |builder|
|
55
|
+
builder.use EY::GateKeeper::Client::Middleware::Authentication, key_method, secret_method, token_setter, impersonation_token_method
|
56
|
+
builder.run inner_app
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Client
|
4
|
+
module Middleware
|
5
|
+
class Authentication
|
6
|
+
|
7
|
+
def initialize(app, key_callback, secret_callback, token_setter, impersonation_token_callback)
|
8
|
+
@app, @key_callback, @secret_callback, @token_setter, @impersonation_token_callback =
|
9
|
+
app, key_callback, secret_callback, token_setter, impersonation_token_callback
|
10
|
+
end
|
11
|
+
|
12
|
+
def need_token?
|
13
|
+
@token.nil? || @token.expired?
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_token_to(env)
|
17
|
+
if @token
|
18
|
+
env.update('HTTP_AUTH_TOKEN' => @token.token)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_impersonation_token_to(env)
|
23
|
+
impersonation_token = @impersonation_token_callback.call
|
24
|
+
if impersonation_token
|
25
|
+
env.update('HTTP_X_IMPERSONATION_TOKEN' => impersonation_token)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def call(env)
|
30
|
+
if need_token?
|
31
|
+
get_token(env.dup)
|
32
|
+
end
|
33
|
+
|
34
|
+
add_token_to(env)
|
35
|
+
add_impersonation_token_to(env)
|
36
|
+
|
37
|
+
app_response = EY::GateKeeper::Client::ServerResponse.new(@app.call(env))
|
38
|
+
return app_response unless @token
|
39
|
+
|
40
|
+
if app_response.needs_gatekeeper_token_refresh?
|
41
|
+
get_token(env.dup)
|
42
|
+
add_token_to(env)
|
43
|
+
app_response = EY::GateKeeper::Client::ServerResponse.new(@app.call(env))
|
44
|
+
end
|
45
|
+
|
46
|
+
app_response.to_rack
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_token(env)
|
50
|
+
if auth_response = authenticate(env)
|
51
|
+
@token = EY::GateKeeper::Token.new(auth_response)
|
52
|
+
else
|
53
|
+
@token = nil
|
54
|
+
end
|
55
|
+
@token_setter.call(@token) if @token_setter
|
56
|
+
end
|
57
|
+
|
58
|
+
def authenticate(env)
|
59
|
+
key = @key_callback.call
|
60
|
+
secret = @secret_callback.call
|
61
|
+
|
62
|
+
env.update('HTTP_AUTHORIZATION' => ["#{key}:#{secret}"].pack("m*").gsub("\n", ""))
|
63
|
+
env.update('REQUEST_PATH' => '/_authenticate')
|
64
|
+
env.update('PATH_INFO' => '/_authenticate')
|
65
|
+
|
66
|
+
auth_response = EY::GateKeeper::Client::ServerResponse.new(@app.call(env))
|
67
|
+
|
68
|
+
if auth_response.status == 200
|
69
|
+
JSON.parse(auth_response.body.join)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Client
|
4
|
+
module Middleware
|
5
|
+
# This middleware is for use by service clients.
|
6
|
+
#
|
7
|
+
# Given the service client's app to run along with a
|
8
|
+
# gatekeeper endpoint, key, and secret it will contact the
|
9
|
+
# gatekeeper and retrieve a token. The token will then be
|
10
|
+
# passed as part of the request in the `Auth-Token` header.
|
11
|
+
# @param app The service client's Rack::Client application (passed automatically when this middleware is `use`d
|
12
|
+
# @param [String] gatekeeper_endpoint URL of the gatekeeper to fetch the token from
|
13
|
+
# @param [String] xdna_key Xdna key to authenticate against the gatekeeper with
|
14
|
+
# @param [String] xdna_secret Xdna secret to authenticate against the gatekeeper with
|
15
|
+
class ServiceTokenAuthentication
|
16
|
+
|
17
|
+
def initialize(app, gatekeeper_endpoint, xdna_key, xdna_secret)
|
18
|
+
@app, @gatekeeper_endpoint, @xdna_key, @xdna_secret =
|
19
|
+
app, gatekeeper_endpoint, xdna_key, xdna_secret
|
20
|
+
end
|
21
|
+
|
22
|
+
def call(env)
|
23
|
+
env.merge!('HTTP_AUTH_TOKEN' => current_token.token)
|
24
|
+
@app.call(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
def gatekeeper_client
|
28
|
+
@gatekeeper_client ||= EY::GateKeeper.new(@xdna_key, @xdna_secret, :endpoint => @gatekeeper_endpoint)
|
29
|
+
end
|
30
|
+
|
31
|
+
def current_token
|
32
|
+
gatekeeper_client.token
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Client
|
4
|
+
class ServerResponse < Rack::Response
|
5
|
+
|
6
|
+
def initialize(response)
|
7
|
+
status, headers, body = response
|
8
|
+
super(body, status, headers)
|
9
|
+
end
|
10
|
+
|
11
|
+
def unauthorized?
|
12
|
+
status == 401
|
13
|
+
end
|
14
|
+
|
15
|
+
def is_gatekeeper_unauthorized?
|
16
|
+
(unauthorized? && headers['X-Gatekeeper-Authentication-Error']) ? true : false
|
17
|
+
end
|
18
|
+
|
19
|
+
def indicates_any_auth_error?(*messages)
|
20
|
+
if auth_error_header = headers['X-Gatekeeper-Authentication-Error']
|
21
|
+
messages.map { |message| EY::GateKeeper::Responses.string(message) }.include?(auth_error_header)
|
22
|
+
else
|
23
|
+
false
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def needs_gatekeeper_token_refresh?
|
28
|
+
is_gatekeeper_unauthorized? && indicates_any_auth_error?(:expired_token, :expired_impersonation_token)
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_rack
|
32
|
+
[status, headers, body]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rack/client'
|
2
|
+
|
3
|
+
module EY
|
4
|
+
module GateKeeper
|
5
|
+
module Client
|
6
|
+
|
7
|
+
def self.master_key
|
8
|
+
'77zxcvbnm77'
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.master_secret
|
12
|
+
'77zxcvbnm77'
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.master_user
|
16
|
+
'xcloud'
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.new(*a)
|
20
|
+
EY::GateKeeper::Client::Consumer.new(*a)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.mock(key = self.master_key, secret = self.master_secret, options = {})
|
24
|
+
mock_options = {
|
25
|
+
:endpoint => 'http://gatekeeper.local/',
|
26
|
+
:app => mock_app(master_key, master_secret, master_user)
|
27
|
+
}
|
28
|
+
|
29
|
+
new(key, secret, mock_options.merge(options))
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.mock_app(master_key = self.master_key, master_secret = self.master_secret, master_user = self.master_user)
|
33
|
+
Rack::Builder.app do |builder|
|
34
|
+
builder.run EY::GateKeeper::Server.app(:master_key => master_key, :master_secret => master_secret, :master_user => master_user)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
require 'ey_gatekeeper/client/middlewares/authentication'
|
42
|
+
require 'ey_gatekeeper/client/middlewares/service_token_authentication'
|
43
|
+
require 'ey_gatekeeper/client/server_response'
|
44
|
+
require 'ey_gatekeeper/client/consumer'
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Responses
|
4
|
+
|
5
|
+
#The canned responses
|
6
|
+
CANNED_RESPONSES = {
|
7
|
+
:unable_to_create_token => 'unable to create token',
|
8
|
+
:invalid_authentication => 'invalid authentication info',
|
9
|
+
:no_authentication => 'no authentication info',
|
10
|
+
:no_token => 'no token specified',
|
11
|
+
:invalid_token => 'invalid token specified',
|
12
|
+
:expired_token => 'expired token specified',
|
13
|
+
:token_insufficient => 'token has insufficient access',
|
14
|
+
:invalid_impersonation_token => 'invalid impersonation token specified',
|
15
|
+
:expired_impersonation_token => 'expired impersonation token specified',
|
16
|
+
:impersonation_token_insufficient => 'impersonation token has insufficient access'
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
#Get the response string via symbol
|
20
|
+
def self.string(lookup = nil)
|
21
|
+
CANNED_RESPONSES.fetch(lookup,'')
|
22
|
+
end
|
23
|
+
|
24
|
+
#A list of possible response symbols
|
25
|
+
def self.possible_responses
|
26
|
+
CANNED_RESPONSES.keys
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
#Standard Unauthorized rsponse. Just add message
|
31
|
+
# @param [String] message the messsage to add into the response
|
32
|
+
#
|
33
|
+
# @return [Array] a standard Rack 3 member array response. The Headers contain the message provided
|
34
|
+
def unauthorized_response(message)
|
35
|
+
[
|
36
|
+
401,
|
37
|
+
{
|
38
|
+
'Content-Type' => 'text/plain',
|
39
|
+
'Content-Length' => '0',
|
40
|
+
'X-Gatekeeper-Authentication-Error' => message.is_a?(Symbol) ? EY::GateKeeper::Responses.string(message) : message.to_s
|
41
|
+
},
|
42
|
+
[]
|
43
|
+
]
|
44
|
+
end
|
45
|
+
|
46
|
+
#Standard token response
|
47
|
+
# @param [Hash] token the hash of the token data
|
48
|
+
# The response body is a json encoded hash and contains the token (keyed as 'token') and it's expiration (keyed as 'expires')
|
49
|
+
#
|
50
|
+
# @return [Array] a standard Rack 3 member array response
|
51
|
+
def token_response(token)
|
52
|
+
response = {
|
53
|
+
'token' => token['uri'].split("/")[2],
|
54
|
+
'expires' => token['expires']
|
55
|
+
}.to_json
|
56
|
+
[
|
57
|
+
200,
|
58
|
+
{
|
59
|
+
'Content-Type' => 'application/json',
|
60
|
+
'Content-Length' => response.length.to_s,
|
61
|
+
},
|
62
|
+
[response]
|
63
|
+
]
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
class Token
|
4
|
+
|
5
|
+
attr_reader :token_data
|
6
|
+
|
7
|
+
def initialize(token_data)
|
8
|
+
@token_data = token_data
|
9
|
+
end
|
10
|
+
|
11
|
+
def expired?
|
12
|
+
if @token_data['expires']
|
13
|
+
Time.now.utc.to_i >= @token_data['expires'].to_i
|
14
|
+
else
|
15
|
+
true
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def token
|
20
|
+
@token_data['token']
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
token
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
module Util
|
4
|
+
|
5
|
+
if ENV['DEBUG_GATEKEEPER']
|
6
|
+
def debug(message)
|
7
|
+
puts message
|
8
|
+
end
|
9
|
+
else
|
10
|
+
def debug(message); end
|
11
|
+
end
|
12
|
+
|
13
|
+
def random_sha
|
14
|
+
Digest::SHA256.hexdigest((1..1000).map { |i| rand.to_s }.join)
|
15
|
+
end
|
16
|
+
|
17
|
+
def root_or_meta?(env)
|
18
|
+
env['PATH_INFO'] == '/' || env['PATH_INFO'] == '/cloudkit-meta'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module EY
|
2
|
+
module GateKeeper
|
3
|
+
def self.new(*a)
|
4
|
+
if mocking?
|
5
|
+
EY::GateKeeper::Client.mock(*a)
|
6
|
+
else
|
7
|
+
EY::GateKeeper::Client.new(*a)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.mock!
|
12
|
+
@mocking = true
|
13
|
+
|
14
|
+
require 'ey_gatekeeper/server'
|
15
|
+
EY::GateKeeper::Server.mock!
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.mocking?
|
19
|
+
@mocking
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.needs_reset?
|
23
|
+
!!@needs_reset
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.was_reset!
|
27
|
+
@needs_reset = false
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.reset!(config = default_mongodb)
|
31
|
+
if mocking?
|
32
|
+
CloudKit.setup_storage_adapter(CloudKit::MemoryTable.new)
|
33
|
+
EY::GateKeeper::AuthStore.setup_master_credentials('77zxcvbnm77','77zxcvbnm77')
|
34
|
+
else
|
35
|
+
raise "Don't use reset! when not mocking"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.default_mongodb
|
40
|
+
{
|
41
|
+
:hosts => [["localhost", 27017]],
|
42
|
+
:safe_write_options => {
|
43
|
+
:fsync => false
|
44
|
+
}
|
45
|
+
}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
require 'ey_gatekeeper/util'
|
51
|
+
require 'ey_gatekeeper/responses'
|
52
|
+
require 'ey_gatekeeper/token'
|
53
|
+
require 'ey_gatekeeper/version'
|
54
|
+
require 'ey_gatekeeper/client'
|