ey_gatekeeper 0.1.34

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.
@@ -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,5 @@
1
+ module EY
2
+ module GateKeeper
3
+ VERSION = '0.1.34'
4
+ end
5
+ 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'