ey_gatekeeper 0.1.34

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