nucleus 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (224) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +19 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +44 -0
  6. data/.travis.yml +21 -0
  7. data/CHANGELOG.md +19 -0
  8. data/CONTRIBUTING.md +13 -0
  9. data/Gemfile +16 -0
  10. data/Guardfile +22 -0
  11. data/LICENSE +21 -0
  12. data/README.md +675 -0
  13. data/Rakefile +137 -0
  14. data/bin/nucleus +91 -0
  15. data/bin/nucleus.bat +1 -0
  16. data/config.ru +18 -0
  17. data/config/adapters/cloud_control.yml +32 -0
  18. data/config/adapters/cloud_foundry_v2.yml +61 -0
  19. data/config/adapters/heroku.yml +13 -0
  20. data/config/adapters/openshift_v2.yml +20 -0
  21. data/config/nucleus_config.rb +47 -0
  22. data/lib/nucleus.rb +13 -0
  23. data/lib/nucleus/adapter_resolver.rb +115 -0
  24. data/lib/nucleus/adapters/base_adapter.rb +109 -0
  25. data/lib/nucleus/adapters/buildpack_translator.rb +79 -0
  26. data/lib/nucleus/adapters/v1/cloud_control/application.rb +108 -0
  27. data/lib/nucleus/adapters/v1/cloud_control/authentication.rb +27 -0
  28. data/lib/nucleus/adapters/v1/cloud_control/buildpacks.rb +23 -0
  29. data/lib/nucleus/adapters/v1/cloud_control/cloud_control.rb +153 -0
  30. data/lib/nucleus/adapters/v1/cloud_control/data.rb +76 -0
  31. data/lib/nucleus/adapters/v1/cloud_control/domains.rb +68 -0
  32. data/lib/nucleus/adapters/v1/cloud_control/lifecycle.rb +27 -0
  33. data/lib/nucleus/adapters/v1/cloud_control/log_poller.rb +71 -0
  34. data/lib/nucleus/adapters/v1/cloud_control/logs.rb +103 -0
  35. data/lib/nucleus/adapters/v1/cloud_control/regions.rb +32 -0
  36. data/lib/nucleus/adapters/v1/cloud_control/scaling.rb +17 -0
  37. data/lib/nucleus/adapters/v1/cloud_control/semantic_errors.rb +31 -0
  38. data/lib/nucleus/adapters/v1/cloud_control/services.rb +162 -0
  39. data/lib/nucleus/adapters/v1/cloud_control/token.rb +17 -0
  40. data/lib/nucleus/adapters/v1/cloud_control/vars.rb +88 -0
  41. data/lib/nucleus/adapters/v1/cloud_foundry_v2/app_states.rb +28 -0
  42. data/lib/nucleus/adapters/v1/cloud_foundry_v2/application.rb +111 -0
  43. data/lib/nucleus/adapters/v1/cloud_foundry_v2/authentication.rb +17 -0
  44. data/lib/nucleus/adapters/v1/cloud_foundry_v2/buildpacks.rb +23 -0
  45. data/lib/nucleus/adapters/v1/cloud_foundry_v2/cloud_foundry_v2.rb +141 -0
  46. data/lib/nucleus/adapters/v1/cloud_foundry_v2/data.rb +97 -0
  47. data/lib/nucleus/adapters/v1/cloud_foundry_v2/domains.rb +149 -0
  48. data/lib/nucleus/adapters/v1/cloud_foundry_v2/lifecycle.rb +41 -0
  49. data/lib/nucleus/adapters/v1/cloud_foundry_v2/logs.rb +303 -0
  50. data/lib/nucleus/adapters/v1/cloud_foundry_v2/regions.rb +33 -0
  51. data/lib/nucleus/adapters/v1/cloud_foundry_v2/scaling.rb +15 -0
  52. data/lib/nucleus/adapters/v1/cloud_foundry_v2/semantic_errors.rb +27 -0
  53. data/lib/nucleus/adapters/v1/cloud_foundry_v2/services.rb +286 -0
  54. data/lib/nucleus/adapters/v1/cloud_foundry_v2/vars.rb +80 -0
  55. data/lib/nucleus/adapters/v1/heroku/app_states.rb +57 -0
  56. data/lib/nucleus/adapters/v1/heroku/application.rb +93 -0
  57. data/lib/nucleus/adapters/v1/heroku/authentication.rb +27 -0
  58. data/lib/nucleus/adapters/v1/heroku/buildpacks.rb +27 -0
  59. data/lib/nucleus/adapters/v1/heroku/data.rb +78 -0
  60. data/lib/nucleus/adapters/v1/heroku/domains.rb +43 -0
  61. data/lib/nucleus/adapters/v1/heroku/heroku.rb +146 -0
  62. data/lib/nucleus/adapters/v1/heroku/lifecycle.rb +51 -0
  63. data/lib/nucleus/adapters/v1/heroku/logs.rb +108 -0
  64. data/lib/nucleus/adapters/v1/heroku/regions.rb +42 -0
  65. data/lib/nucleus/adapters/v1/heroku/scaling.rb +28 -0
  66. data/lib/nucleus/adapters/v1/heroku/semantic_errors.rb +23 -0
  67. data/lib/nucleus/adapters/v1/heroku/services.rb +168 -0
  68. data/lib/nucleus/adapters/v1/heroku/vars.rb +65 -0
  69. data/lib/nucleus/adapters/v1/openshift_v2/app_states.rb +68 -0
  70. data/lib/nucleus/adapters/v1/openshift_v2/application.rb +108 -0
  71. data/lib/nucleus/adapters/v1/openshift_v2/authentication.rb +21 -0
  72. data/lib/nucleus/adapters/v1/openshift_v2/data.rb +96 -0
  73. data/lib/nucleus/adapters/v1/openshift_v2/domains.rb +37 -0
  74. data/lib/nucleus/adapters/v1/openshift_v2/lifecycle.rb +60 -0
  75. data/lib/nucleus/adapters/v1/openshift_v2/logs.rb +106 -0
  76. data/lib/nucleus/adapters/v1/openshift_v2/openshift_v2.rb +125 -0
  77. data/lib/nucleus/adapters/v1/openshift_v2/regions.rb +58 -0
  78. data/lib/nucleus/adapters/v1/openshift_v2/scaling.rb +39 -0
  79. data/lib/nucleus/adapters/v1/openshift_v2/semantic_errors.rb +40 -0
  80. data/lib/nucleus/adapters/v1/openshift_v2/services.rb +173 -0
  81. data/lib/nucleus/adapters/v1/openshift_v2/vars.rb +49 -0
  82. data/lib/nucleus/adapters/v1/stub_adapter.rb +464 -0
  83. data/lib/nucleus/core/adapter_authentication_inductor.rb +62 -0
  84. data/lib/nucleus/core/adapter_extensions/auth/auth_client.rb +44 -0
  85. data/lib/nucleus/core/adapter_extensions/auth/authentication_retry_wrapper.rb +79 -0
  86. data/lib/nucleus/core/adapter_extensions/auth/expiring_token_auth_client.rb +53 -0
  87. data/lib/nucleus/core/adapter_extensions/auth/http_basic_auth_client.rb +37 -0
  88. data/lib/nucleus/core/adapter_extensions/auth/o_auth2_auth_client.rb +95 -0
  89. data/lib/nucleus/core/adapter_extensions/auth/token_auth_client.rb +36 -0
  90. data/lib/nucleus/core/adapter_extensions/http_client.rb +177 -0
  91. data/lib/nucleus/core/adapter_extensions/http_tail_client.rb +26 -0
  92. data/lib/nucleus/core/adapter_extensions/tail_stopper.rb +25 -0
  93. data/lib/nucleus/core/common/errors/ambiguous_adapter_error.rb +7 -0
  94. data/lib/nucleus/core/common/errors/file_existence_error.rb +7 -0
  95. data/lib/nucleus/core/common/errors/startup_error.rb +12 -0
  96. data/lib/nucleus/core/common/exit_codes.rb +25 -0
  97. data/lib/nucleus/core/common/files/application_repo_sanitizer.rb +52 -0
  98. data/lib/nucleus/core/common/files/archive_extractor.rb +112 -0
  99. data/lib/nucleus/core/common/files/archiver.rb +91 -0
  100. data/lib/nucleus/core/common/link_generator.rb +46 -0
  101. data/lib/nucleus/core/common/logging/logging.rb +52 -0
  102. data/lib/nucleus/core/common/logging/multi_logger.rb +59 -0
  103. data/lib/nucleus/core/common/logging/request_log_formatter.rb +48 -0
  104. data/lib/nucleus/core/common/ssh_handler.rb +108 -0
  105. data/lib/nucleus/core/common/stream_callback.rb +27 -0
  106. data/lib/nucleus/core/common/thread_config_accessor.rb +85 -0
  107. data/lib/nucleus/core/common/url_converter.rb +28 -0
  108. data/lib/nucleus/core/enums/application_states.rb +26 -0
  109. data/lib/nucleus/core/enums/logfile_types.rb +28 -0
  110. data/lib/nucleus/core/error_messages.rb +127 -0
  111. data/lib/nucleus/core/errors/adapter_error.rb +13 -0
  112. data/lib/nucleus/core/errors/adapter_missing_implementation_error.rb +12 -0
  113. data/lib/nucleus/core/errors/adapter_request_error.rb +10 -0
  114. data/lib/nucleus/core/errors/adapter_resource_not_found_error.rb +10 -0
  115. data/lib/nucleus/core/errors/endpoint_authentication_error.rb +10 -0
  116. data/lib/nucleus/core/errors/platform_specific_semantic_error.rb +12 -0
  117. data/lib/nucleus/core/errors/platform_timeout_error.rb +10 -0
  118. data/lib/nucleus/core/errors/platform_unavailable_error.rb +10 -0
  119. data/lib/nucleus/core/errors/semantic_adapter_request_error.rb +19 -0
  120. data/lib/nucleus/core/errors/unknown_adapter_call_error.rb +10 -0
  121. data/lib/nucleus/core/file_handling/archive_converter.rb +29 -0
  122. data/lib/nucleus/core/file_handling/file_manager.rb +64 -0
  123. data/lib/nucleus/core/file_handling/git_deployer.rb +133 -0
  124. data/lib/nucleus/core/file_handling/git_repo_analyzer.rb +23 -0
  125. data/lib/nucleus/core/import/adapter_configuration.rb +53 -0
  126. data/lib/nucleus/core/import/vendor_parser.rb +28 -0
  127. data/lib/nucleus/core/import/version_detector.rb +18 -0
  128. data/lib/nucleus/core/models/abstract_model.rb +29 -0
  129. data/lib/nucleus/core/models/endpoint.rb +30 -0
  130. data/lib/nucleus/core/models/provider.rb +26 -0
  131. data/lib/nucleus/core/models/vendor.rb +22 -0
  132. data/lib/nucleus/ext/kernel.rb +5 -0
  133. data/lib/nucleus/ext/regexp.rb +49 -0
  134. data/lib/nucleus/os.rb +15 -0
  135. data/lib/nucleus/root_dir.rb +13 -0
  136. data/lib/nucleus/scripts/finalize.rb +8 -0
  137. data/lib/nucleus/scripts/initialize.rb +9 -0
  138. data/lib/nucleus/scripts/initialize_config_defaults.rb +26 -0
  139. data/lib/nucleus/scripts/load.rb +17 -0
  140. data/lib/nucleus/scripts/load_dependencies.rb +43 -0
  141. data/lib/nucleus/scripts/setup_config.rb +28 -0
  142. data/lib/nucleus/scripts/shutdown.rb +11 -0
  143. data/lib/nucleus/version.rb +3 -0
  144. data/nucleus.gemspec +88 -0
  145. data/public/robots.txt +2 -0
  146. data/public/swagger-ui/css/reset.css +125 -0
  147. data/public/swagger-ui/css/screen.css +1224 -0
  148. data/public/swagger-ui/images/apple-touch-icon-114x114.png +0 -0
  149. data/public/swagger-ui/images/apple-touch-icon-120x120.png +0 -0
  150. data/public/swagger-ui/images/apple-touch-icon-144x144.png +0 -0
  151. data/public/swagger-ui/images/apple-touch-icon-152x152.png +0 -0
  152. data/public/swagger-ui/images/apple-touch-icon-57x57.png +0 -0
  153. data/public/swagger-ui/images/apple-touch-icon-60x60.png +0 -0
  154. data/public/swagger-ui/images/apple-touch-icon-72x72.png +0 -0
  155. data/public/swagger-ui/images/apple-touch-icon-76x76.png +0 -0
  156. data/public/swagger-ui/images/explorer_icons.png +0 -0
  157. data/public/swagger-ui/images/favicon-128.png +0 -0
  158. data/public/swagger-ui/images/favicon-16x16.png +0 -0
  159. data/public/swagger-ui/images/favicon-196x196.png +0 -0
  160. data/public/swagger-ui/images/favicon-32x32.png +0 -0
  161. data/public/swagger-ui/images/favicon-96x96.png +0 -0
  162. data/public/swagger-ui/images/favicon.ico +0 -0
  163. data/public/swagger-ui/images/logo_small.png +0 -0
  164. data/public/swagger-ui/images/mstile-144x144.png +0 -0
  165. data/public/swagger-ui/images/mstile-150x150.png +0 -0
  166. data/public/swagger-ui/images/mstile-310x150.png +0 -0
  167. data/public/swagger-ui/images/mstile-310x310.png +0 -0
  168. data/public/swagger-ui/images/mstile-70x70.png +0 -0
  169. data/public/swagger-ui/images/pet_store_api.png +0 -0
  170. data/public/swagger-ui/images/throbber.gif +0 -0
  171. data/public/swagger-ui/images/wordnik_api.png +0 -0
  172. data/public/swagger-ui/index.html +107 -0
  173. data/public/swagger-ui/lib/backbone-min.js +38 -0
  174. data/public/swagger-ui/lib/handlebars-1.0.0.js +2278 -0
  175. data/public/swagger-ui/lib/highlight.7.3.pack.js +1 -0
  176. data/public/swagger-ui/lib/jquery-1.8.0.min.js +2 -0
  177. data/public/swagger-ui/lib/jquery.ba-bbq.min.js +18 -0
  178. data/public/swagger-ui/lib/jquery.slideto.min.js +1 -0
  179. data/public/swagger-ui/lib/jquery.wiggle.min.js +8 -0
  180. data/public/swagger-ui/lib/shred.bundle.js +2765 -0
  181. data/public/swagger-ui/lib/shred/content.js +193 -0
  182. data/public/swagger-ui/lib/swagger-oauth.js +211 -0
  183. data/public/swagger-ui/lib/swagger.js +1653 -0
  184. data/public/swagger-ui/lib/underscore-min.js +32 -0
  185. data/public/swagger-ui/o2c.html +15 -0
  186. data/public/swagger-ui/redirect.html +14 -0
  187. data/public/swagger-ui/swagger-ui.js +2324 -0
  188. data/public/swagger-ui/swagger-ui.min.js +1 -0
  189. data/schemas/api.adapter.schema.yml +31 -0
  190. data/schemas/api.requirements.schema.yml +17 -0
  191. data/spec/factories/models.rb +61 -0
  192. data/spec/integration/api/auth_spec.rb +58 -0
  193. data/spec/integration/api/endpoints_spec.rb +167 -0
  194. data/spec/integration/api/errors_spec.rb +47 -0
  195. data/spec/integration/api/providers_spec.rb +157 -0
  196. data/spec/integration/api/swagger_schema_spec.rb +64 -0
  197. data/spec/integration/api/vendors_spec.rb +45 -0
  198. data/spec/integration/integration_spec_helper.rb +27 -0
  199. data/spec/integration/test_data_generator.rb +55 -0
  200. data/spec/nucleus_git_key.pem +51 -0
  201. data/spec/spec_helper.rb +98 -0
  202. data/spec/support/shared_example_request_types.rb +99 -0
  203. data/spec/test_suites.rake +31 -0
  204. data/spec/unit/adapters/archive_converter_spec.rb +25 -0
  205. data/spec/unit/adapters/file_manager_spec.rb +93 -0
  206. data/spec/unit/adapters/git_deployer_spec.rb +262 -0
  207. data/spec/unit/adapters/v1/stub_spec.rb +14 -0
  208. data/spec/unit/common/helpers/auth_helper_spec.rb +73 -0
  209. data/spec/unit/common/oauth2_auth_client_spec.rb +108 -0
  210. data/spec/unit/common/regexp_spec.rb +33 -0
  211. data/spec/unit/common/request_log_formatter_spec.rb +108 -0
  212. data/spec/unit/common/thread_config_accessor_spec.rb +97 -0
  213. data/spec/unit/models/endpoint_spec.rb +83 -0
  214. data/spec/unit/models/provider_spec.rb +102 -0
  215. data/spec/unit/models/vendor_spec.rb +100 -0
  216. data/spec/unit/schemas/adapter_schema_spec.rb +16 -0
  217. data/spec/unit/schemas/adapter_validation_spec.rb +56 -0
  218. data/spec/unit/schemas/requirements_schema_spec.rb +16 -0
  219. data/spec/unit/unit_spec_helper.rb +11 -0
  220. data/tasks/compatibility.rake +113 -0
  221. data/tasks/evaluation.rake +162 -0
  222. data/wiki/adapter_tests.md +99 -0
  223. data/wiki/implement_new_adapter.md +155 -0
  224. metadata +836 -0
@@ -0,0 +1,62 @@
1
+ module Nucleus
2
+ # The {AdapterAuthenticationInductor} patches adapter classes so that each method that is defined in the AdapterStub
3
+ # of the current API version is wrapped with the {Nucleus::Adapters::AuthenticationRetryWrapper}.
4
+ module AdapterAuthenticationInductor
5
+ include Nucleus::Logging
6
+
7
+ # Patch the adapter instance and use the authentication information of the environment.
8
+ # @param [Nucleus::Adapters::BaseAdapter] adapter_instance the adapter implementation instance to patch
9
+ # @param [Hash<String, String>] env environment which includes the HTTP authentication header
10
+ # @return [void]
11
+ def self.patch(adapter_instance, env)
12
+ stub_class(adapter_instance).instance_methods(false).each do |method_to_wrap|
13
+ # wrap method with authentication repetition call
14
+ patch_method(adapter_instance, method_to_wrap, env)
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Patch the actual method that is defined in an API version stub.
21
+ # The method shall than be able to update the authentication token if the initial authentication expired.<br>
22
+ # Only major authentication issues, e.g. if the credentials are repeatedly rejected,
23
+ # will be thrown to the adapter caller.
24
+ # @param [Nucleus::Adapters::BaseAdapter] adapter the adapter implementation to patch
25
+ # @param [Symbol] method_to_wrap method that shall be patched
26
+ # @param [Hash<String, String>] env environment which includes the HTTP authentication header
27
+ # @return [void]
28
+ # @private
29
+ def self.patch_method(adapter, method_to_wrap, env)
30
+ with_wrapper = :"#{method_to_wrap}_with_before_each_method_call"
31
+ without_wrapper = :"#{method_to_wrap}_without_before_each_method_call"
32
+ # patching should be done only once, return if method is already patched (!)
33
+ return if adapter.respond_to?(with_wrapper) && adapter.respond_to?(without_wrapper)
34
+
35
+ @__last_methods_added = [method_to_wrap, with_wrapper, without_wrapper]
36
+ # wrap the method call
37
+ adapter.class.send :define_method, with_wrapper do |*args, &block|
38
+ log.debug "Calling adapter method '#{method_to_wrap}' against #{endpoint_url}"
39
+ # use the AuthenticationRetryWrapper to retry calls if tokens expired, ...
40
+ Nucleus::Adapters::AuthenticationRetryWrapper.with_authentication(adapter, env) do
41
+ return send without_wrapper, *args, &block
42
+ end
43
+ end
44
+ # now do the actual method re-assignment
45
+ adapter.class.send :define_method, without_wrapper, adapter.method(method_to_wrap)
46
+ adapter.class.send :define_method, method_to_wrap, adapter.method(with_wrapper)
47
+ @__last_methods_added = nil
48
+ end
49
+ private_class_method :patch_method
50
+
51
+ # @private
52
+ def self.stub_class(adapter)
53
+ parent = adapter.class
54
+ loop do
55
+ break if parent.superclass == Adapters::BaseAdapter
56
+ parent = parent.superclass
57
+ end
58
+ parent
59
+ end
60
+ private_class_method :stub_class
61
+ end
62
+ end
@@ -0,0 +1,44 @@
1
+ module Nucleus
2
+ module Adapters
3
+ class AuthClient
4
+ attr_reader :verify_ssl
5
+
6
+ # Create a new instance of an {AuthClient}.
7
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
8
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
9
+ def initialize(check_certificates = true)
10
+ @verify_ssl = check_certificates
11
+ end
12
+
13
+ # Perform authentication with the given username and password at the desired endpoint.
14
+ # @param[String] username username to use for authentication
15
+ # @param[String] password password to the username, which is to be used for authentication
16
+ # @raise[Nucleus::Errors::EndpointAuthenticationError] if authentication failed
17
+ # @raise[Nucleus::Errors::UnknownAdapterCallError] if the generic AuthClient did expect the endpoint
18
+ # to behave differently, usually indicates implementation issues
19
+ # @return[Nucleus::Adapters::AuthClient] current AuthClient instance
20
+ def authenticate(username, password)
21
+ fail Errors::EndpointAuthenticationError, 'Authentication client does not support authentication'
22
+ end
23
+
24
+ # Get the authentication header for the current AuthClient instance that must be used to execute requests
25
+ # against the endpoint.<br>
26
+ # If the authentication is known to be expired, a refresh will be made first.
27
+ # @raise[Nucleus::Errors::EndpointAuthenticationError] if the refresh failed
28
+ # @return[Hash<String, String>] authentication header that enables requests against the endpoint
29
+ def auth_header
30
+ fail Errors::EndpointAuthenticationError,
31
+ 'Authentication client does not support to create the authentication header'
32
+ end
33
+
34
+ # Refresh a rejected authentication and generate a new authentication header.<br>
35
+ # Should be called if the authentication is known to be expired, or when a request is rejected with an
36
+ # authentication header that used to be accepted.
37
+ # @raise [Nucleus::Errors::EndpointAuthenticationError] if token refresh failed or authentication never succeeded
38
+ # @return [Nucleus::Adapters::AuthClient] current AuthClient instance
39
+ def refresh
40
+ fail Errors::EndpointAuthenticationError, 'Authentication client does not support refresh'
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,79 @@
1
+ module Nucleus
2
+ module Adapters
3
+ # The AuthenticationRetryWrapper module can be used to invoke commands in a block that repeats its execution in case
4
+ # the first attempt raises an {Nucleus::Errors::EndpointAuthenticationError}.
5
+ module AuthenticationRetryWrapper
6
+ extend Nucleus::Logging
7
+
8
+ # Executes a block, which should be an adapter call, using the authentication information.
9
+ # If the first call fails due to cached authentication information, the cache is going to get evicted,
10
+ # authentication repeated and finally the call will be executed again.
11
+ #
12
+ # @param [Nucleus::Adapters::BaseAdapter] adapter adapter that is used for the ongoing request
13
+ # @param [Hash<String, String>] env Rack environment, shall contain HTTP authentication information
14
+ # @return [Hash, void] result of the yield block execution, usually a Hash matching the Grape::Entity to represent
15
+ def self.with_authentication(adapter, env)
16
+ begin
17
+ response = yield
18
+ rescue Errors::EndpointAuthenticationError
19
+ # first attempt with actually valid credentials failed, try to refresh token based clients
20
+ username, password = username_password(env)
21
+ begin
22
+ auth_client = adapter.cached(adapter.cache_key(username, password))
23
+ auth_client.refresh
24
+ response = yield
25
+ rescue Errors::EndpointAuthenticationError
26
+ # refresh failed, too
27
+ log.debug 'Call failed (401), start repetition by removing outdated cache entry'
28
+ re_authenticate(adapter, env)
29
+ log.debug 'Repeating call block...'
30
+ response = yield
31
+ log.debug '... the repetition did pass just fine!'
32
+ end
33
+ end
34
+ response
35
+ end
36
+
37
+ # Try to refresh a token based authentication that can be renewed.
38
+ # The method shall only be invoked when there are cached authentication information
39
+ # that appear to be outdated.<br>
40
+ # If the refresh fails, a complete re-authentication will be forced.
41
+ #
42
+ # @param [Nucleus::Adapters::AuthClient] auth_client platform specific version of the authentication client
43
+ # @raise [Nucleus::Errors::EndpointAuthenticationError] if both, refresh and authentication fail
44
+ # @return [void]
45
+ def self.refresh_token(auth_client)
46
+ # we first try to renew our token before invalidating the cache
47
+ log.debug 'Call failed (401), start refreshing auth token'
48
+ auth_client.refresh unless auth_client.nil?
49
+ log.debug '... the auth token refresh succeeded'
50
+ end
51
+
52
+ # Re-authenticate the user with the help of the current adapter.
53
+ # The method shall only be invoked when there are cached authentication information that appear to be outdated.
54
+ # It calls the authentication for the current user to override the cached authentication headers.
55
+ #
56
+ # @param [Nucleus::Adapters::BaseAdapter] adapter adapter that is used for the ongoing request
57
+ # @param [Hash<String, String>] env Rack environment, shall contain HTTP authentication information
58
+ # @raise [Nucleus::Errors::EndpointAuthenticationError] if authentication at the endpoint fails
59
+ # @return [void]
60
+ def self.re_authenticate(adapter, env)
61
+ log.debug('Invoked re-authentication')
62
+ username, password = username_password(env)
63
+ auth_client = adapter.cached(adapter.cache_key(username, password))
64
+ # raises 401 if the authentication did not only expire, but became completely invalid
65
+ auth_client.authenticate(username, password)
66
+ end
67
+
68
+ # Extract the username and password from the current HTTP request.
69
+ # @param [Hash<String, String>] env Rack environment, shall contain HTTP authentication information
70
+ # @return [Array<String>] username at response[0], password at response[1]
71
+ def self.username_password(env)
72
+ # resolve username & password for authentication request
73
+ auth_keys = %w(HTTP_AUTHORIZATION X-HTTP_AUTHORIZATION X_HTTP_AUTHORIZATION)
74
+ authorization_key = auth_keys.detect { |k| env.key?(k) }
75
+ env[authorization_key].split(' ', 2).last.unpack('m*').first.split(/:/, 2)
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,53 @@
1
+ module Nucleus
2
+ module Adapters
3
+ class ExpiringTokenAuthClient < TokenAuthClient
4
+ # Create a new instance of an {ExpiringTokenAuthClient}. An expiring token knows when it starts to be invalid,
5
+ # saving requests to the endpoint that would fail anyways.
6
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
7
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
8
+ # @yield [verify_ssl, username, password] Auth credentials token parser block,
9
+ # must provide the API token and its expiration date, usually retrieved from an HTTP call to the endpoint.
10
+ # @yieldparam [Boolean] verify_ssl true if SSL certificates are to be validated,
11
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
12
+ # @yieldparam [String] username username to be used to retrieve the API token
13
+ # @yieldparam [String] password password to be used to retrieve the API token
14
+ # @yieldreturn [Array<String>] Array with 2 contents:
15
+ # [0] = API token to be used for authenticated API requests,
16
+ # nil if authentication failed, e.g. due to bad credentials
17
+ # [1] = Expiration time until the token is valid
18
+ def initialize(check_certificates = true, &token_expiration_parser)
19
+ @token_expiration_parser = token_expiration_parser
20
+ super(check_certificates)
21
+ end
22
+
23
+ # @see AuthClient#authenticate
24
+ def authenticate(username, password)
25
+ token, expiration_time = @token_expiration_parser.call(verify_ssl, username, password)
26
+ fail Errors::EndpointAuthenticationError, 'Authentication failed, credentials seem to be invalid' unless token
27
+ # verification passed, credentials are valid
28
+ @api_token = token
29
+ @expires = expiration_time
30
+ self
31
+ end
32
+
33
+ # @see AuthClient#auth_header
34
+ def auth_header
35
+ fail Errors::EndpointAuthenticationError, 'Authentication client was not authenticated yet' unless @api_token
36
+ fail Errors::EndpointAuthenticationError, 'Cached authentication token expired' if expired?
37
+ { 'Authorization' => "Bearer #{api_token}" }
38
+ end
39
+
40
+ private
41
+
42
+ # Checks if the token is expired.
43
+ # @return [true, false] true if the token is expired, false if it is still valid
44
+ def expired?
45
+ @expires >= Time.now
46
+ end
47
+
48
+ def to_s
49
+ api_token
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,37 @@
1
+ module Nucleus
2
+ module Adapters
3
+ # Implementation of the AuthClient that works with the HTTP basic authentication.
4
+ class HttpBasicAuthClient < AuthClient
5
+ # Create a new instance of an {HttpBasicAuthClient}.
6
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
7
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
8
+ # @yield [verify_ssl, username, password] Auth credentials verification block,
9
+ # must check if the combination of username and password is accepted by the endpoint.
10
+ # @yieldparam [Hash<String,String>] headers headers for an HTTP request,
11
+ # including the authentication header to be tested
12
+ # @yieldreturn [Boolean] true if the authentication was verified to be ok,
13
+ # false if an error occurred, e.g. with bad credentials
14
+ def initialize(check_certificates = true, &verification)
15
+ @verification = verification
16
+ super(check_certificates)
17
+ end
18
+
19
+ # @see AuthClient#authenticate
20
+ def authenticate(username, password)
21
+ packed_credentials = ["#{username}:#{password}"].pack('m*').gsub(/\n/, '')
22
+ valid = @verification.call(verify_ssl, 'Authorization' => "Basic #{packed_credentials}")
23
+ fail Errors::EndpointAuthenticationError, 'Authentication failed, credentials seem to be invalid' unless valid
24
+ # verification passed, credentials are valid
25
+ @packed_credentials = packed_credentials
26
+ self
27
+ end
28
+
29
+ # @see AuthClient#auth_header
30
+ def auth_header
31
+ fail Errors::EndpointAuthenticationError,
32
+ 'Authentication client was not authenticated yet' unless @packed_credentials
33
+ { 'Authorization' => "Basic #{@packed_credentials}" }
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,95 @@
1
+ module Nucleus
2
+ module Adapters
3
+ class OAuth2AuthClient < AuthClient
4
+ include Nucleus::Logging
5
+
6
+ # Create a new instance of an {OAuth2AuthClient}, which uses the standardized OAuth2 authentication method.
7
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
8
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
9
+ # @param [String] auth_url URL to the OAuth2 endpoint
10
+ def initialize(auth_url, check_certificates = true)
11
+ @auth_url = auth_url
12
+ super(check_certificates)
13
+ end
14
+
15
+ def authenticate(username, password)
16
+ return self if @access_token
17
+ response = post(query: { grant_type: 'password', username: username, password: password })
18
+ body = body(response)
19
+ extract(body)
20
+ # refresh token is not included in later updates
21
+ @refresh_token = body[:refresh_token]
22
+ self
23
+ end
24
+
25
+ def auth_header
26
+ fail Errors::EndpointAuthenticationError, 'Authentication client was not authenticated yet' unless @access_token
27
+ if expired?
28
+ log.debug('OAuth2 access_token is expired, trigger refresh before returning auth_header')
29
+ # token is expired, renew first
30
+ refresh
31
+ end
32
+ # then return the authorization header
33
+ header
34
+ end
35
+
36
+ def refresh
37
+ if @refresh_token.nil?
38
+ fail Errors::EndpointAuthenticationError, "Can't refresh token before initial authentication"
39
+ end
40
+ log.debug("Attempt to refresh the access_token with our refresh_token: '#{@refresh_token}'")
41
+ response = post(query: { grant_type: 'refresh_token', refresh_token: @refresh_token })
42
+ extract(body(response))
43
+ self
44
+ end
45
+
46
+ private
47
+
48
+ def post(params)
49
+ middleware = Excon.defaults[:middlewares].dup
50
+ middleware << Excon::Middleware::Decompress
51
+ middleware << Excon::Middleware::RedirectFollower
52
+ # explicitly allow redirects, otherwise they would cause an error
53
+ # TODO: Basic Y2Y6 could be cloud-foundry specific
54
+ request_params = { expects: [200, 301, 302, 303, 307, 308], middlewares: middleware.uniq,
55
+ headers: { 'Authorization' => 'Basic Y2Y6',
56
+ 'Content-Type' => 'application/x-www-form-urlencoded',
57
+ 'Accept' => 'application/json' } }.merge(params)
58
+ # execute the post request and return the response
59
+ Excon.new(@auth_url, ssl_verify_peer: verify_ssl).post(request_params)
60
+ rescue Excon::Errors::HTTPStatusError => e
61
+ log.debug "OAuth2 authentication failed: #{e}"
62
+ case e.response.status
63
+ when 403
64
+ log.error("OAuth2 for '#{@auth_url}' failed with status 403 (access denied), indicating an adapter issue")
65
+ raise Errors::UnknownAdapterCallError, 'Access to resource denied, probably the adapter must be updated'
66
+ when 400, 401
67
+ raise Errors::EndpointAuthenticationError, body(e.response)[:error_description]
68
+ end
69
+ # re-raise all unhandled exception, indicating adapter issues
70
+ raise Errors::UnknownAdapterCallError, 'OAuth2 call failed unexpectedly, probably the adapter must be updated'
71
+ end
72
+
73
+ def header
74
+ { 'Authorization' => "#{@token_type} #{@access_token}" }
75
+ end
76
+
77
+ def expired?
78
+ return true if @expiration.nil?
79
+ Time.now >= @expiration
80
+ end
81
+
82
+ def body(response)
83
+ Oj.load(response.body, symbol_keys: true)
84
+ end
85
+
86
+ def extract(body)
87
+ @access_token = body[:access_token]
88
+ # number of seconds until expiration, deduct processing buffer
89
+ seconds_left = body[:expires_in] - 30
90
+ @expiration = Time.now + seconds_left
91
+ @token_type = body[:token_type]
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,36 @@
1
+ module Nucleus
2
+ module Adapters
3
+ class TokenAuthClient < AuthClient
4
+ attr_reader :api_token
5
+
6
+ # Create a new instance of an {TokenAuthClient}.
7
+ # @param [Boolean] check_certificates true if SSL certificates are to be validated,
8
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
9
+ # @yield [verify_ssl, username, password] Auth credentials token parser block,
10
+ # must provide the API token, usually retrieved from an HTTP call to the endpoint.
11
+ # @yieldparam [Boolean] verify_ssl true if SSL certificates are to be validated,
12
+ # false if they are to be ignored (e.g. when using self-signed certificates in development environments)
13
+ # @yieldparam [String] username username to be used to retrieve the API token
14
+ # @yieldparam [String] password password to be used to retrieve the API token
15
+ # @yieldreturn [String] API token to be used for authenticated API requests,
16
+ # nil if authentication failed, e.g. due to bad credentials
17
+ def initialize(check_certificates = true, &token_parser)
18
+ @token_parser = token_parser
19
+ super(check_certificates)
20
+ end
21
+
22
+ def authenticate(username, password)
23
+ token = @token_parser.call(verify_ssl, username, password)
24
+ fail Errors::EndpointAuthenticationError, 'Authentication failed, credentials seem to be invalid' unless token
25
+ # verification passed, credentials are valid
26
+ @api_token = token
27
+ self
28
+ end
29
+
30
+ def auth_header
31
+ fail Errors::EndpointAuthenticationError, 'Authentication client was not authenticated yet' unless @api_token
32
+ { 'Authorization' => "Bearer #{api_token}" }
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,177 @@
1
+ module Nucleus
2
+ module Adapters
3
+ module HttpClient
4
+ # Executes a HEAD request to the given URL.
5
+ #
6
+ # @param [String] path path to add to the endpoint URL
7
+ # @param [Hash] params options to call the post request with
8
+ # @option params [Array<int>] :expects ([200]) http status code that is expected
9
+ # @option params [Hash] :headers request headers to use with the request
10
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
11
+ # unprocessed response
12
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
13
+ def head(path, params = {})
14
+ execute_request(:head, [200], path, params, params.delete(:native_call) { false })
15
+ end
16
+
17
+ # Executes a GET request to the given URL.
18
+ #
19
+ # @param [String] path path to add to the endpoint URL
20
+ # @param [Hash] params options to call the post request with
21
+ # @option params [Array<int>] :expects ([200]) http status code that is expected
22
+ # @option params [Hash] :headers request headers to use with the request
23
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
24
+ # unprocessed response
25
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
26
+ def get(path, params = {})
27
+ execute_request(:get, [200], path, params, params.delete(:native_call) { false })
28
+ end
29
+
30
+ # Executes a POST request to the given URL.
31
+ #
32
+ # @param [String] path path to add to the endpoint URL
33
+ # @param [Hash] params options to call the post request with
34
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
35
+ # @option params [Hash] :body request body, will be converted to json format
36
+ # @option params [Hash] :headers request headers to use with the request
37
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
38
+ # unprocessed response
39
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
40
+ def post(path, params = {})
41
+ execute_request(:post, [200, 201], path, params, params.delete(:native_call) { false })
42
+ end
43
+
44
+ # Executes a PATCH request to the given URL.
45
+ #
46
+ # @param [String] path path to add to the endpoint URL
47
+ # @param [Hash] params options to call the post request with
48
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
49
+ # @option params [Hash] :body request body, will be converted to json format
50
+ # @option params [Hash] :headers request headers to use with the request
51
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
52
+ # unprocessed response
53
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
54
+ def patch(path, params = {})
55
+ execute_request(:patch, [200, 201], path, params, params.delete(:native_call) { false })
56
+ end
57
+
58
+ # Executes a PUT request to the given URL.
59
+ #
60
+ # @param [String] path path to add to the endpoint URL
61
+ # @param [Hash] params options to call the post request with
62
+ # @option params [Array<int>] :expects ([200,201]) http status code that is expected
63
+ # @option params [Hash] :body request body, will be converted to json format
64
+ # @option params [Hash] :headers request headers to use with the request
65
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
66
+ # unprocessed response
67
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
68
+ def put(path, params = {})
69
+ execute_request(:put, [200, 201], path, params, params.delete(:native_call) { false })
70
+ end
71
+
72
+ # Executes a DELETE request to the given URL.
73
+ #
74
+ # @param [String] path path to add to the endpoint URL
75
+ # @param [Hash] params options to call the post request with
76
+ # @option params [Array<int>] :expects ([200,204]) http status code that is expected
77
+ # @option params [Hash] :headers request headers to use with the request
78
+ # @option params [Boolean] :native_call if true the request is a native API call and shall return the
79
+ # unprocessed response
80
+ # @raise [Nucleus::Errors::AdapterError] if the call failed and did not return the expected code(s)
81
+ def delete(path, params = {})
82
+ execute_request(:delete, [200, 204], path, params, params.delete(:native_call) { false })
83
+ end
84
+
85
+ private
86
+
87
+ def execute_request(method, default_expect, path, params, native_call = false)
88
+ params[:expects] = default_expect unless params.key? :expects
89
+ params[:method] = method
90
+
91
+ url = Regexp::PERFECT_URL_PATTERN =~ path ? path : to_url(path)
92
+ response = Excon.new(url, excon_connection_params(params)).request(add_common_request_params(params))
93
+ # we never want the JSON string, but always the hash representation
94
+ response.body = hash_of(response.body)
95
+ response
96
+ rescue Excon::Errors::HTTPStatusError => e
97
+ handle_execute_request_error(e, url, native_call)
98
+ end
99
+
100
+ def handle_execute_request_error(e, url, native_call)
101
+ log.debug 'ERROR, Excon could not execute the request.'
102
+ # transform json response to Hash object
103
+ e.response.body = hash_of(e.response.body)
104
+
105
+ # if this is a native API call, do not further process the error
106
+ return e.response if native_call
107
+
108
+ # fail with adapter specific error handling
109
+ handle_error(e.response) if respond_to?(:handle_error)
110
+ fallback_error_handling(e, url)
111
+ end
112
+
113
+ def fallback_error_handling(e, url)
114
+ error_status = e.response.status
115
+ # arriving here, error could not be processed --> use fallback errors
116
+ if e.is_a? Excon::Errors::ServerError
117
+ fail Errors::UnknownAdapterCallError, e.message
118
+ elsif error_status == 404
119
+ log.error("Resource not found (404) at '#{url}', indicating an adapter issue")
120
+ fail Errors::UnknownAdapterCallError, 'Resource not found, probably the adapter must be updated'
121
+ elsif error_status == 401
122
+ fail Errors::EndpointAuthenticationError,
123
+ 'Auth. failed, probably cache is outdated or permissions were revoked?'
124
+ else
125
+ log.error("Fallback error handling (#{error_status}) at '#{url}', indicating an adapter issue")
126
+ fail Errors::UnknownAdapterCallError, e.message
127
+ end
128
+ end
129
+
130
+ def to_url(path)
131
+ # insert missing slash, prevent double slashes
132
+ return "#{@endpoint_url}/#{path}" unless @endpoint_url.end_with?('/') || path.start_with?('/')
133
+ "#{@endpoint_url}#{path}"
134
+ end
135
+
136
+ def excon_connection_params(params)
137
+ middleware = Excon.defaults[:middlewares].dup
138
+
139
+ if params[:follow_redirects] == false
140
+ middleware = [Excon::Middleware::ResponseParser, Excon::Middleware::Decompress].push(*middleware).uniq
141
+ else
142
+ middleware = [Excon::Middleware::ResponseParser, Excon::Middleware::RedirectFollower,
143
+ Excon::Middleware::Decompress].push(*middleware).uniq
144
+ end
145
+ { middlewares: middleware, ssl_verify_peer: @check_certificates }
146
+ end
147
+
148
+ def hash_of(message_body)
149
+ return {} if message_body.nil? || message_body.empty?
150
+ begin
151
+ return Oj.load(message_body, symbol_keys: true)
152
+ rescue Oj::Error
153
+ # parsing failed, content probably is no valid JSON content
154
+ message_body
155
+ end
156
+ end
157
+
158
+ def add_common_request_params(params)
159
+ common_params = { connection_timeout: 610, write_timeout: 600, read_timeout: 600 }
160
+ # allow to follow redirects in the APIs
161
+ allowed_status_codes = params.key?(:expects) ? [*params[:expects]] : []
162
+ unless params[:follow_redirects] == false
163
+ allowed_status_codes.push(*[301, 302, 303, 307, 308])
164
+ end
165
+
166
+ params[:expects] = allowed_status_codes.uniq
167
+ # use default or customized headers
168
+ params[:headers] = headers unless params[:headers]
169
+ # specify encoding if not done yet: use only gzip since deflate does cause issues with VCR cassettes in tests
170
+ params[:headers]['Accept-Encoding'] = 'gzip' unless params[:headers].key? 'Accept-Encoding'
171
+ params[:body] = params[:body].to_json if params.key? :body
172
+ # merge and return
173
+ common_params.merge params
174
+ end
175
+ end
176
+ end
177
+ end