nucleus 0.1.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.
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