apisonator 2.100.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 (173) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +317 -0
  3. data/Gemfile +11 -0
  4. data/Gemfile.base +65 -0
  5. data/Gemfile.lock +319 -0
  6. data/Gemfile.on_prem +1 -0
  7. data/Gemfile.on_prem.lock +297 -0
  8. data/LICENSE +202 -0
  9. data/NOTICE +15 -0
  10. data/README.md +230 -0
  11. data/Rakefile +287 -0
  12. data/apisonator.gemspec +47 -0
  13. data/app/api/api.rb +13 -0
  14. data/app/api/internal/alert_limits.rb +32 -0
  15. data/app/api/internal/application_keys.rb +49 -0
  16. data/app/api/internal/application_referrer_filters.rb +43 -0
  17. data/app/api/internal/applications.rb +77 -0
  18. data/app/api/internal/errors.rb +54 -0
  19. data/app/api/internal/events.rb +42 -0
  20. data/app/api/internal/internal.rb +104 -0
  21. data/app/api/internal/metrics.rb +40 -0
  22. data/app/api/internal/service_tokens.rb +46 -0
  23. data/app/api/internal/services.rb +58 -0
  24. data/app/api/internal/stats.rb +42 -0
  25. data/app/api/internal/usagelimits.rb +62 -0
  26. data/app/api/internal/utilization.rb +23 -0
  27. data/bin/3scale_backend +223 -0
  28. data/bin/3scale_backend_worker +26 -0
  29. data/config.ru +4 -0
  30. data/config/puma.rb +192 -0
  31. data/config/schedule.rb +9 -0
  32. data/ext/mkrf_conf.rb +64 -0
  33. data/lib/3scale/backend.rb +67 -0
  34. data/lib/3scale/backend/alert_limit.rb +56 -0
  35. data/lib/3scale/backend/alerts.rb +137 -0
  36. data/lib/3scale/backend/analytics/kinesis.rb +3 -0
  37. data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
  38. data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
  39. data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
  40. data/lib/3scale/backend/analytics/redshift.rb +3 -0
  41. data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
  42. data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
  43. data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
  44. data/lib/3scale/backend/application.rb +330 -0
  45. data/lib/3scale/backend/application_events.rb +76 -0
  46. data/lib/3scale/backend/background_job.rb +65 -0
  47. data/lib/3scale/backend/configurable.rb +20 -0
  48. data/lib/3scale/backend/configuration.rb +151 -0
  49. data/lib/3scale/backend/configuration/loader.rb +42 -0
  50. data/lib/3scale/backend/constants.rb +19 -0
  51. data/lib/3scale/backend/cors.rb +84 -0
  52. data/lib/3scale/backend/distributed_lock.rb +67 -0
  53. data/lib/3scale/backend/environment.rb +21 -0
  54. data/lib/3scale/backend/error_storage.rb +52 -0
  55. data/lib/3scale/backend/errors.rb +343 -0
  56. data/lib/3scale/backend/event_storage.rb +120 -0
  57. data/lib/3scale/backend/experiment.rb +84 -0
  58. data/lib/3scale/backend/extensions.rb +5 -0
  59. data/lib/3scale/backend/extensions/array.rb +19 -0
  60. data/lib/3scale/backend/extensions/hash.rb +26 -0
  61. data/lib/3scale/backend/extensions/nil_class.rb +13 -0
  62. data/lib/3scale/backend/extensions/redis.rb +44 -0
  63. data/lib/3scale/backend/extensions/string.rb +13 -0
  64. data/lib/3scale/backend/extensions/time.rb +110 -0
  65. data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
  66. data/lib/3scale/backend/job_fetcher.rb +122 -0
  67. data/lib/3scale/backend/listener.rb +728 -0
  68. data/lib/3scale/backend/listener_metrics.rb +99 -0
  69. data/lib/3scale/backend/logging.rb +48 -0
  70. data/lib/3scale/backend/logging/external.rb +44 -0
  71. data/lib/3scale/backend/logging/external/impl.rb +93 -0
  72. data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
  73. data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
  74. data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
  75. data/lib/3scale/backend/logging/external/resque.rb +57 -0
  76. data/lib/3scale/backend/logging/logger.rb +18 -0
  77. data/lib/3scale/backend/logging/middleware.rb +62 -0
  78. data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
  79. data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
  80. data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
  81. data/lib/3scale/backend/logging/worker.rb +107 -0
  82. data/lib/3scale/backend/manifest.rb +80 -0
  83. data/lib/3scale/backend/memoizer.rb +277 -0
  84. data/lib/3scale/backend/metric.rb +275 -0
  85. data/lib/3scale/backend/metric/collection.rb +91 -0
  86. data/lib/3scale/backend/oauth.rb +4 -0
  87. data/lib/3scale/backend/oauth/token.rb +26 -0
  88. data/lib/3scale/backend/oauth/token_key.rb +30 -0
  89. data/lib/3scale/backend/oauth/token_storage.rb +313 -0
  90. data/lib/3scale/backend/oauth/token_value.rb +25 -0
  91. data/lib/3scale/backend/period.rb +3 -0
  92. data/lib/3scale/backend/period/boundary.rb +107 -0
  93. data/lib/3scale/backend/period/cache.rb +28 -0
  94. data/lib/3scale/backend/period/period.rb +402 -0
  95. data/lib/3scale/backend/queue_storage.rb +16 -0
  96. data/lib/3scale/backend/rack.rb +49 -0
  97. data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
  98. data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
  99. data/lib/3scale/backend/rack/prometheus.rb +19 -0
  100. data/lib/3scale/backend/saas.rb +6 -0
  101. data/lib/3scale/backend/saas_analytics.rb +4 -0
  102. data/lib/3scale/backend/server.rb +30 -0
  103. data/lib/3scale/backend/server/falcon.rb +52 -0
  104. data/lib/3scale/backend/server/puma.rb +71 -0
  105. data/lib/3scale/backend/service.rb +317 -0
  106. data/lib/3scale/backend/service_token.rb +97 -0
  107. data/lib/3scale/backend/stats.rb +8 -0
  108. data/lib/3scale/backend/stats/aggregator.rb +170 -0
  109. data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
  110. data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
  111. data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
  112. data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
  113. data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
  114. data/lib/3scale/backend/stats/cleaner.rb +195 -0
  115. data/lib/3scale/backend/stats/codes_commons.rb +14 -0
  116. data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
  117. data/lib/3scale/backend/stats/key_generator.rb +73 -0
  118. data/lib/3scale/backend/stats/keys.rb +104 -0
  119. data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
  120. data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
  121. data/lib/3scale/backend/stats/period_commons.rb +34 -0
  122. data/lib/3scale/backend/stats/stats_parser.rb +141 -0
  123. data/lib/3scale/backend/stats/storage.rb +113 -0
  124. data/lib/3scale/backend/statsd.rb +14 -0
  125. data/lib/3scale/backend/storable.rb +35 -0
  126. data/lib/3scale/backend/storage.rb +40 -0
  127. data/lib/3scale/backend/storage_async.rb +4 -0
  128. data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
  129. data/lib/3scale/backend/storage_async/client.rb +205 -0
  130. data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
  131. data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
  132. data/lib/3scale/backend/storage_helpers.rb +278 -0
  133. data/lib/3scale/backend/storage_key_helpers.rb +9 -0
  134. data/lib/3scale/backend/storage_sync.rb +43 -0
  135. data/lib/3scale/backend/transaction.rb +62 -0
  136. data/lib/3scale/backend/transactor.rb +177 -0
  137. data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
  138. data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
  139. data/lib/3scale/backend/transactor/notify_job.rb +47 -0
  140. data/lib/3scale/backend/transactor/process_job.rb +33 -0
  141. data/lib/3scale/backend/transactor/report_job.rb +84 -0
  142. data/lib/3scale/backend/transactor/status.rb +236 -0
  143. data/lib/3scale/backend/transactor/usage_report.rb +182 -0
  144. data/lib/3scale/backend/usage.rb +63 -0
  145. data/lib/3scale/backend/usage_limit.rb +115 -0
  146. data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
  147. data/lib/3scale/backend/util.rb +17 -0
  148. data/lib/3scale/backend/validators.rb +26 -0
  149. data/lib/3scale/backend/validators/base.rb +36 -0
  150. data/lib/3scale/backend/validators/key.rb +17 -0
  151. data/lib/3scale/backend/validators/limits.rb +57 -0
  152. data/lib/3scale/backend/validators/oauth_key.rb +15 -0
  153. data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
  154. data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
  155. data/lib/3scale/backend/validators/referrer.rb +60 -0
  156. data/lib/3scale/backend/validators/service_state.rb +15 -0
  157. data/lib/3scale/backend/validators/state.rb +15 -0
  158. data/lib/3scale/backend/version.rb +5 -0
  159. data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
  160. data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
  161. data/lib/3scale/backend/worker.rb +87 -0
  162. data/lib/3scale/backend/worker_async.rb +88 -0
  163. data/lib/3scale/backend/worker_metrics.rb +44 -0
  164. data/lib/3scale/backend/worker_sync.rb +32 -0
  165. data/lib/3scale/bundler_shim.rb +17 -0
  166. data/lib/3scale/prometheus_server.rb +10 -0
  167. data/lib/3scale/tasks/connectivity.rake +41 -0
  168. data/lib/3scale/tasks/helpers.rb +3 -0
  169. data/lib/3scale/tasks/helpers/environment.rb +23 -0
  170. data/lib/3scale/tasks/stats.rake +131 -0
  171. data/lib/3scale/tasks/swagger.rake +46 -0
  172. data/licenses.xml +1215 -0
  173. metadata +227 -0
@@ -0,0 +1,122 @@
1
+ module ThreeScale
2
+ module Backend
3
+ class JobFetcher
4
+ include Resque::Helpers
5
+ include Configurable
6
+
7
+ # the order is relevant
8
+ QUEUES = [:priority, :main, :stats].freeze
9
+ private_constant :QUEUES
10
+
11
+ REDIS_TIMEOUT = 60
12
+ private_constant :REDIS_TIMEOUT
13
+
14
+ DEFAULT_MAX_PENDING_JOBS = 100
15
+ private_constant :DEFAULT_MAX_PENDING_JOBS
16
+
17
+ DEFAULT_WAIT_BEFORE_FETCHING_MORE_JOBS = 1.0/100
18
+ private_constant :DEFAULT_WAIT_BEFORE_FETCHING_MORE_JOBS
19
+
20
+ RedisConnectionError = Class.new(RuntimeError)
21
+
22
+ # The default redis_client is the one defined in Resque::Helpers
23
+ def initialize(redis_client: redis, fetch_timeout: REDIS_TIMEOUT)
24
+ @redis = redis_client
25
+ @fetch_timeout = fetch_timeout
26
+ @queues ||= QUEUES.map { |q| "queue:#{q}" }
27
+
28
+ @max_pending_jobs = configuration.async_worker.max_pending_jobs ||
29
+ DEFAULT_MAX_PENDING_JOBS
30
+
31
+ @wait_before_fetching_more = configuration.async_worker.seconds_before_fetching_more ||
32
+ DEFAULT_WAIT_BEFORE_FETCHING_MORE_JOBS
33
+ end
34
+
35
+ def pop_from_queue
36
+ begin
37
+ encoded_job = @redis.blpop(*@queues, timeout: @fetch_timeout)
38
+ rescue Redis::BaseConnectionError, Errno::ECONNREFUSED, Errno::EPIPE => e
39
+ raise RedisConnectionError.new(e.message)
40
+ rescue Redis::CommandError => e
41
+ # Redis::CommandError from redis-rb can be raised for multiple
42
+ # reasons, so we need to check the error message to distinguish
43
+ # connection errors from the rest.
44
+ if e.message == 'ERR Connection timed out'.freeze
45
+ raise RedisConnectionError.new(e.message)
46
+ else
47
+ raise e
48
+ end
49
+ end
50
+
51
+ encoded_job
52
+ end
53
+
54
+ def fetch
55
+ encoded_job = pop_from_queue
56
+ return nil if encoded_job.nil? || encoded_job.empty?
57
+
58
+ begin
59
+ # Resque::Job.new accepts a queue name as a param. It is very
60
+ # important to set here the same name as the one we set when calling
61
+ # Resque.enqueue. Resque.enqueue uses the @queue ivar in
62
+ # BackgroundJob classes as the name of the queue, and then, it stores
63
+ # the job in a queue called resque:queue:_@queue_. 'resque:' is the
64
+ # namespace and 'queue:' is added automatically. That's why we need
65
+ # to call blpop on 'queue:#{q}' above. However, when creating the job
66
+ # we cannot set 'queue:#{q}' as the name. Otherwise, if it fails and
67
+ # it is re-queued, it will end up in resque:queue:queue:_@queue_
68
+ # instead of resque:queue:_@queue_.
69
+ encoded_job[0].sub!('queue:', '')
70
+ Resque::Job.new(encoded_job[0],
71
+ Yajl::Parser.parse(encoded_job[1], check_utf8: false))
72
+ rescue Exception => e
73
+ # I think that the only exception that can be raised here is
74
+ # Yajl::ParseError. However, this is a critical part of the code so
75
+ # we will capture all of them just to be safe.
76
+ Worker.logger.notify(e)
77
+ nil
78
+ end
79
+ end
80
+
81
+ # Note: this method calls #close on job_queue after receiving #shutdown.
82
+ # That signals to the caller that there won't be any more jobs.
83
+ def start(job_queue)
84
+ loop do
85
+ break if @shutdown
86
+
87
+ if job_queue.size >= @max_pending_jobs
88
+ sleep @wait_before_fetching_more
89
+ else
90
+ begin
91
+ job = fetch
92
+ rescue RedisConnectionError => e
93
+ # If there has been a connection error or a timeout we wait a bit
94
+ # because normally, it will be a temporary problem.
95
+ # In the future, we might want to put a limit in the total number
96
+ # of attempts or implement exponential backoff retry times.
97
+ Worker.logger.notify(e)
98
+ sleep(1)
99
+
100
+ # Re-instantiate Redis instance. This is needed to recover from
101
+ # Errno::EPIPE, not sure if there are others.
102
+ @redis = ThreeScale::Backend::QueueStorage.connection(
103
+ ThreeScale::Backend.environment,
104
+ ThreeScale::Backend.configuration
105
+ )
106
+ # If there is a different kind of error, it's probably a
107
+ # programming error. Like sending an invalid blpop command to
108
+ # Redis. In that case, let the worker crash.
109
+ end
110
+ job_queue << job if job
111
+ end
112
+ end
113
+
114
+ job_queue.close
115
+ end
116
+
117
+ def shutdown
118
+ @shutdown = true
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,728 @@
1
+ require '3scale/backend/version'
2
+ require '3scale/backend/cors'
3
+ require 'json'
4
+
5
+ module ThreeScale
6
+ module Backend
7
+ class Listener < Sinatra::Base
8
+ disable :logging
9
+ enable :raise_errors
10
+ disable :show_exceptions
11
+
12
+ include Logging
13
+
14
+ ## ------------ DOCS --------------
15
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
16
+ ##~ sapi = source2swagger.namespace(namespace)
17
+ ##~ sapi.basePath = "https://su1.3scale.net"
18
+ ##~ sapi.swaggerVersion = "0.1a"
19
+ ##~ sapi.apiVersion = "1.0"
20
+ ##
21
+ ## ------------ DOCS COMMON -------
22
+ ##~ @parameter_service_token = {"name" => "service_token", "dataType" => "string", "required" => true, "paramType" => "query", "threescale_name" => "service_tokens"}
23
+ ##~ @parameter_service_token["description"] = "Your service api key with 3scale (also known as service token)."
24
+ ##
25
+ ##~ @parameter_service_id = {"name" => "service_id", "dataType" => "string", "required" => true, "paramType" => "query", "threescale_name" => "service_ids"}
26
+ ##~ @parameter_service_id["description"] = "Service id. Required."
27
+ ##
28
+ ##~ @parameter_app_id = {"name" => "app_id", "dataType" => "string", "required" => true, "paramType" => "query", "threescale_name" => "app_ids"}
29
+ ##~ @parameter_app_id["description"] = "App Id (identifier of the application if the auth. pattern is App Id)"
30
+ ##~ @parameter_app_id_inline = @parameter_app_id.clone
31
+ ##~ @parameter_app_id_inline["description_inline"] = true
32
+ ##
33
+ ##~ @parameter_access_token = {"name" => "access_token", "dataType" => "string", "required" => false, "paramType" => "query", "threescale_name" => "access_tokens"}
34
+ ##~ @parameter_access_token["description"] = "OAuth token used for authorizing if you don't use client_id with client_secret."
35
+ ##
36
+ ##~ @parameter_client_id = {"name" => "app_id", "dataType" => "string", "required" => false, "paramType" => "query", "threescale_name" => "app_ids"}
37
+ ##~ @parameter_client_id["description"] = "Client Id (identifier of the application if the auth. pattern is OAuth, note that client_id == app_id)"
38
+ ##~ @parameter_client_id_inline = @parameter_client_id.clone
39
+ ##~ @parameter_client_id_inline["description_inline"] = true
40
+
41
+ ##~ @parameter_app_key = {"name" => "app_key", "dataType" => "string", "required" => false, "paramType" => "query", "threescale_name" => "app_keys"}
42
+ ##~ @parameter_app_key["description"] = "App Key (shared secret of the application if the authentication pattern is App Id). The app key is required if the application has one or more keys defined."
43
+ ##
44
+ ##~ @parameter_user_key = {"name" => "user_key", "dataType" => "string", "required" => true, "paramType" => "query", "theescale_name" => "user_keys"}
45
+ ##~ @parameter_user_key["description"] = "User Key (identifier and shared secret of the application if the auth. pattern is Api Key)"
46
+ ##~ @parameter_user_key_inline = @parameter_user_key.clone
47
+ ##~ @parameter_user_key_inline["description_inline"] = true
48
+
49
+ ##~ @parameter_referrer = {"name" => "referrer", "dataType" => "string", "required" => false, "paramType" => "query"}
50
+ ##~ @parameter_referrer["description"] = "Referrer IP Address or Domain. Required only if referrer filtering is enabled. If special value '*' (wildcard) is passed, the referrer check is bypassed."
51
+ ##
52
+ ##~ @parameter_redirect_url = {"name" => "redirect_url", "dataType" => "string", "required" => false, "paramType" => "query"}
53
+ ##~ @parameter_redirect_url["description"] = "Optional redirect URL for OAuth. Will be validated if sent."
54
+ ##
55
+ ##~ @parameter_redirect_uri = {"name" => "redirect_uri", "dataType" => "string", "required" => false, "paramType" => "query"}
56
+ ##~ @parameter_redirect_uri["description"] = "Optional redirect URI for OAuth. This is the same as 'redirect_url', but if used you should expect a matching 'redirect_uri' response field."
57
+ ##
58
+
59
+ ##~ @parameter_usage = {"name" => "usage", "dataType" => "hash", "required" => false, "paramType" => "query", "allowMultiple" => false}
60
+ ##~ @parameter_usage["description"] = "Usage will increment the metrics with the values passed. The value can be only a positive integer (e.g. 1, 50). Reporting usage[hits]=1 will increment the hits counter by +1."
61
+ ##
62
+ ##~ @parameter_usage_fields = {"name" => "metric", "dataType" => "custom", "required" => false, "paramType" => "query", "allowMultiple" => true, "threescale_name" => "metric_names"}
63
+ ##~ @parameter_usage_fields["description"] = "Metric to be reported"
64
+ ##
65
+ ##~ @parameter_usage["parameters"] = []
66
+ ##~ @parameter_usage["parameters"] << @parameter_usage_fields
67
+ ##
68
+ ##~ @parameter_usage_predicted = {"name" => "usage", "dataType" => "hash", "required" => false, "paramType" => "query", "allowMultiple" => false}
69
+ ##~ @parameter_usage_predicted["description"] = "Predicted Usage. Actual usage will need to be reported with a report or an authrep."
70
+ ##
71
+ ##~ @parameter_usage_predicted["parameters"] = []
72
+ ##~ @parameter_usage_predicted["parameters"] << @parameter_usage_fields
73
+ ##
74
+ ##~ @timestamp = {"name" => "timestamp", "dataType" => "string", "required" => false, "paramType" => "query"}
75
+ ##~ @timestamp["description"] = "If passed, it should be the time when the transaction took place. Format: Either a UNIX UTC timestamp (seconds from the UNIX Epoch), or YYYY-MM-DD HH:MM:SS for UTC, add -HH:MM or +HH:MM for time offset. For instance, 2011-12-30 22:15:31 -08:00."
76
+ ##~ @timestamp["description_inline"] = true
77
+ ##
78
+ ##~ @parameter_log = {"name" => "log", "dataType" => "hash", "required" => false, "paramType" => "query", "allowMultiple" => false}
79
+ ##~ @parameter_log["description"] = "Request Log allows to log status codes of your API back to 3scale to maintain a log of the latest activity on your API. Request Logs are optional and not available in all 3scale plans."
80
+ ##
81
+ ##~ @parameter_log_field_code = {"name" => "code", "dataType" => "string", "paramType" => "query", "description_inline" => true}
82
+ ##~ @parameter_log_field_code["description"] = "Response code of the response from your API (needs to be URL encoded). Optional. Truncated after 32bytes."
83
+
84
+
85
+ ##~ @parameter_log["parameters"] = []
86
+ ##~ @parameter_log["parameters"] << @parameter_log_field_code
87
+ ##
88
+ ##~ @parameter_transaction_app_id = {"name" => "transactions", "dataType" => "array", "required" => true, "paramType" => "query", "allowMultiple" => true}
89
+ ##~ @parameter_transaction_app_id["description"] = "Transactions to be reported. There is a limit of 1000 transactions to be reported on a single request."
90
+ ##~ @parameter_transaction_app_id["parameters"] = []
91
+ ##
92
+ ##~ @parameter_transaction_app_id["parameters"] << @parameter_app_id_inline
93
+ ##~ @parameter_transaction_app_id["parameters"] << @timestamp
94
+ ##~ @parameter_transaction_app_id["parameters"] << @parameter_usage
95
+ ##~ @parameter_transaction_app_id["parameters"] << @parameter_log
96
+
97
+ ##~ @parameter_transaction_api_key = {"name" => "transactions", "dataType" => "array", "required" => true, "paramType" => "query", "allowMultiple" => true}
98
+ ##~ @parameter_transaction_api_key["description"] = "Transactions to be reported. There is a limit of 1000 transactions to be reported on a single request."
99
+ ##~ @parameter_transaction_api_key["parameters"] = []
100
+
101
+ ##~ @parameter_transaction_api_key["parameters"] << @parameter_user_key_inline
102
+ ##~ @parameter_transaction_api_key["parameters"] << @timestamp
103
+ ##~ @parameter_transaction_api_key["parameters"] << @parameter_usage
104
+ ##~ @parameter_transaction_api_key["parameters"] << @parameter_log
105
+
106
+ ##~ @parameter_transaction_oauth = {"name" => "transactions", "dataType" => "array", "required" => true, "paramType" => "query", "allowMultiple" => true}
107
+ ##~ @parameter_transaction_oauth["description"] = "Transactions to be reported. There is a limit of 1000 transactions to be reported on a single request."
108
+ ##~ @parameter_transaction_oauth["parameters"] = []
109
+
110
+ ##~ @parameter_transaction_oauth["parameters"] << @parameter_client_id_inline
111
+ ##~ @parameter_transaction_oauth["parameters"] << @timestamp
112
+ ##~ @parameter_transaction_oauth["parameters"] << @parameter_usage
113
+ ##~ @parameter_transaction_oauth["parameters"] << @parameter_log
114
+
115
+
116
+ AUTH_AUTHREP_COMMON_PARAMS = ['service_id'.freeze, 'app_id'.freeze, 'app_key'.freeze,
117
+ 'user_key'.freeze, 'provider_key'.freeze,
118
+ 'access_token'.freeze].freeze
119
+ private_constant :AUTH_AUTHREP_COMMON_PARAMS
120
+
121
+ REPORT_EXPECTED_PARAMS = ['provider_key'.freeze,
122
+ 'service_token'.freeze,
123
+ 'service_id'.freeze,
124
+ 'transactions'.freeze].freeze
125
+ private_constant :REPORT_EXPECTED_PARAMS
126
+
127
+ configure :production do
128
+ disable :dump_errors
129
+ end
130
+
131
+ set :views, File.dirname(__FILE__) + '/views'
132
+
133
+ use Backend::Rack::ExceptionCatcher
134
+
135
+ before do
136
+ content_type 'application/vnd.3scale-v2.0+xml'.freeze
137
+ # enable CORS for all our endpoints
138
+ response.headers.merge!(CORS.headers)
139
+ end
140
+
141
+ # Enable CORS pre-flight request for all our endpoints
142
+ options '*' do
143
+ response.headers.merge!(CORS.options_headers)
144
+ 204
145
+ end
146
+
147
+ # this is an HAProxy-specific endpoint, equivalent to
148
+ # their '/haproxy?monitor' one, just renamed to available.
149
+ # returning 200 here means we're up willing to take requests.
150
+ # returning 404 makes HAProxy consider us down soon(ish),
151
+ # taking into account that usually several HAProxies contain
152
+ # us in their listener pool and it takes for all of them to
153
+ # notice before no request is received.
154
+ head '/available' do
155
+ 200
156
+ end
157
+
158
+ def do_api_method(method_name)
159
+ halt 403 if params.nil?
160
+
161
+ normalize_non_empty_keys!
162
+
163
+ provider_key = params[:provider_key] ||
164
+ provider_key_from(params[:service_token], params[:service_id])
165
+
166
+ raise_provider_key_error(params) if blank?(provider_key)
167
+
168
+ check_no_user_id
169
+
170
+ halt 403 unless valid_usage_params?
171
+
172
+ # As params is passed to other methods, we need to overwrite the
173
+ # provider key. Some methods assume that params[:provider_key] is
174
+ # not null/empty.
175
+ params[:provider_key] = provider_key
176
+
177
+ log_without_unused_attrs(params[:log]) if params[:log]
178
+
179
+ auth_status = Transactor.send method_name, provider_key, params, request: request_info
180
+ response_auth_call(auth_status)
181
+ rescue ThreeScale::Backend::Error => error
182
+ begin
183
+ ErrorStorage.store(service_id, error, response_code: 403, request: request_info)
184
+ rescue ProviderKeyInvalid
185
+ # This happens trying to load the service id
186
+ ensure
187
+ raise error
188
+ end
189
+ end
190
+ private :do_api_method
191
+
192
+ ## ------------ DOCS --------------
193
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
194
+ ##~ sapi = source2swagger.namespace(namespace)
195
+ ##~ a = sapi.apis.add
196
+ ##~ a.set "path" => "/transactions/authorize.xml", "format" => "xml"
197
+ ##~ op = a.operations.add
198
+ ##~ op.set :httpMethod => "GET"
199
+ ##~ op.summary = "Authorize (App Id authentication pattern)"
200
+ ##
201
+ ##~ @authorize_desc = "<p>It is used to check if a particular application exists,"
202
+ ##~ @authorize_desc = @authorize_desc + " is active and is within its usage limits. It can be optionally used to authenticate a call using an application key."
203
+ ##~ @authorize_desc = @authorize_desc + " It's possible to pass a 'predicted usage' to the authorize call. This can serve three purposes:<p>1) To make sure an API"
204
+ ##~ @authorize_desc = @authorize_desc + " call won't go over the limits before the call is made, if the usage of the call is known in advance. In this case, the"
205
+ ##~ @authorize_desc = @authorize_desc + " estimated usage can be passed to the authorize call, and it will respond whether the actual API call is still within limit."
206
+ ##~ @authorize_desc = @authorize_desc + " <p>2) To limit the authorization only to a subset of metrics. If usage is passed in, only the metrics listed in it will"
207
+ ##~ @authorize_desc = @authorize_desc + " be checked against the limits. For example: There are two metrics defined: <em>searches</em> and <em>updates</em>. <em>updates</em> are already over"
208
+ ##~ @authorize_desc = @authorize_desc + " limit, but <em>searches</em> are not. In this case, the user should still be allowed to do a search call, but not an update one."
209
+ ##~ @authorize_desc = @authorize_desc + " And, <p>3) If no usage is passed then any metric with a limit exceeded state will result in an _authorization_failed_ response."
210
+ ##~ @authorize_desc = @authorize_desc + "<p><b>Note:</b> Even if the predicted usage is passed in, authorize is still a <b>read-only</b> operation. You have to make the report call"
211
+ ##~ @authorize_desc = @authorize_desc + " to report the usage."
212
+ ##
213
+ ##~ @authorize_desc_response = "<p>The response can have an http response code: <code class='http'>200</code> OK (if authorization is granted), <code class='http'>409</code> (if it's not granted, typically application over limits or keys missing, check 'reason' tag), "
214
+ ##~ @authorize_desc_response = @authorize_desc_response + " or <code class='http'>403</code> (for authentication errors, check 'error' tag) and <code class='http'>404</code> (not found)."
215
+
216
+ ##~ op.description = "<p>Read-only operation to authorize an application in the App Id authentication pattern." + " "+ @authorize_desc + " " + @authorize_desc_response
217
+ ##~ op.group = "authorize"
218
+ ##
219
+ ##~ op.parameters.add @parameter_service_token
220
+ ##~ op.parameters.add @parameter_service_id
221
+ ##~ op.parameters.add @parameter_app_id
222
+ ##~ op.parameters.add @parameter_app_key
223
+ ##~ op.parameters.add @parameter_referrer
224
+ ##~ op.parameters.add @parameter_usage_predicted
225
+ ##
226
+ ##~ a = sapi.apis.add
227
+ ##~ a.set "path" => "/transactions/authorize.xml", "format" => "xml"
228
+ ##~ op = a.operations.add
229
+ ##~ op.set :httpMethod => "GET", :tags => ["authorize","user_key"], :nickname => "authorize_user_key", :deprecated => false
230
+ ##~ op.summary = "Authorize (API Key authentication pattern)"
231
+ ##
232
+ ##~ op.description = "Read-only operation to authorize an application in the App Key authentication pattern." + " "+ @authorize_desc + " " + @authorize_desc_response
233
+ ##~ op.group = "authorize"
234
+ ##
235
+ ##~ op.parameters.add @parameter_service_token
236
+ ##~ op.parameters.add @parameter_service_id
237
+ ##~ op.parameters.add @parameter_user_key
238
+ ##~ op.parameters.add @parameter_referrer
239
+ ##~ op.parameters.add @parameter_usage_predicted
240
+ ##
241
+ get '/transactions/authorize.xml' do
242
+ do_api_method :authorize
243
+ end
244
+
245
+ ## ------------ DOCS --------------
246
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
247
+ ##~ sapi = source2swagger.namespace(namespace)
248
+ ##~ a = sapi.apis.add
249
+ ##~ a.set "path" => "/transactions/oauth_authorize.xml", "format" => "xml"
250
+ ##~ op = a.operations.add
251
+ ##~ op.set :httpMethod => "GET", :tags => ["authorize","user_key"], :nickname => "oauth_authorize", :deprecated => false
252
+ ##~ op.summary = "Authorize (OAuth authentication mode pattern)"
253
+ ##
254
+ ##~ op.description = "<p>Read-only operation to authorize an application in the OAuth authentication pattern."
255
+ ##~ @oauth_security = "<p>When using this endpoint please pay attention at your handling of app_id and app_key parameters. If you don't specify an app_key, the endpoint assumes the app_id specified has already been authenticated by other means. If you specify the app_key parameter, even if it is empty, it will be checked against the application's keys. If you don't trust the app_id value you have, either use app keys and specify one or use access_token and avoid the app_id parameter."
256
+ ##~ @oauth_desc_response = "<p>This call returns extra data (secret and redirect_url) needed to power OAuth APIs. It's only available for users with OAuth enabled APIs."
257
+ ##~ op.description = op.description + @oauth_security + @oauth_desc_response
258
+ ##~ op.description = op.description + " " + @authorize_desc + " " + @authorize_desc_response
259
+ ##~ @parameter_app_key_oauth = {"name" => "app_key", "dataType" => "string", "required" => false, "paramType" => "query", "threescale_name" => "app_keys"}
260
+ ##~ @parameter_app_key_oauth["description"] = "App Key (shared secret of the application). The app key, if present, must match a key defined for the application. Note that empty values are considered invalid."
261
+ #
262
+ ##~ op.group = "authorize"
263
+ ##
264
+ ##~ op.parameters.add @parameter_service_token
265
+ ##~ op.parameters.add @parameter_service_id
266
+ ##~ op.parameters.add @parameter_access_token
267
+ ##~ op.parameters.add @parameter_client_id
268
+ ##~ op.parameters.add @parameter_app_key_oauth
269
+ ##~ op.parameters.add @parameter_referrer
270
+ ##~ op.parameters.add @parameter_usage_predicted
271
+ ##~ op.parameters.add @parameter_redirect_url
272
+ ##~ op.parameters.add @parameter_redirect_uri
273
+ ##
274
+ get '/transactions/oauth_authorize.xml' do
275
+ do_api_method :oauth_authorize
276
+ end
277
+
278
+ ## ------------ DOCS --------------
279
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
280
+ ##~ sapi = source2swagger.namespace(namespace)
281
+ ##~ a = sapi.apis.add
282
+ ##~ a.set "path" => "/transactions/authrep.xml", "format" => "xml"
283
+ ##~ op = a.operations.add
284
+ ##~ op.set :httpMethod => "GET"
285
+ ##~ op.summary = "AuthRep (Authorize + Report for the App Id authentication pattern)"
286
+ ##
287
+ ##~ @authrep_desc_base = "<p>Authrep is a <b>'one-shot'</b> operation to authorize an application and report the associated transaction at the same time."
288
+ ##~ @authrep_desc = "<p>The main difference between this call and the regular authorize call is that"
289
+ ##~ @authrep_desc = @authrep_desc + " usage will be reported if the authorization is successful. Authrep is the most convenient way to integrate your API with the"
290
+ ##~ @authrep_desc = @authrep_desc + " 3scale's Service Manangement API since it does a 1:1 mapping between a request to your API and a request to 3scale's API."
291
+ ##~ @authrep_desc = @authrep_desc + "<p>If you do not want to do a request to 3scale for each request to your API or batch the reports you should use the Authorize and Report methods instead."
292
+ ##~ @authrep_desc = @authrep_desc + "<p>Authrep is <b>not a read-only</b> operation and will increment the values if the authorization step is a success."
293
+ ##
294
+ ##~ op.description = @authrep_desc_base + @authrep_desc
295
+ ##~ op.group = "authrep"
296
+ ##
297
+ ##~ op.parameters.add @parameter_service_token
298
+ ##~ op.parameters.add @parameter_service_id
299
+ ##~ op.parameters.add @parameter_app_id
300
+ ##~ op.parameters.add @parameter_app_key
301
+ ##~ op.parameters.add @parameter_referrer
302
+ ##~ op.parameters.add @parameter_usage
303
+ ##~ op.parameters.add @parameter_log
304
+ ##
305
+ ##
306
+ ##~ a = sapi.apis.add
307
+ ##~ a.set "path" => "/transactions/authrep.xml", "format" => "xml"
308
+ ##~ op = a.operations.add
309
+ ##~ op.set :httpMethod => "GET"
310
+ ##~ op.summary = "AuthRep (Authorize + Report for the API Key authentication pattern)"
311
+ ##~ op.description = @authrep_desc_base + @authrep_desc
312
+ ##~ op.group = "authrep"
313
+ ##
314
+ ##~ op.parameters.add @parameter_service_token
315
+ ##~ op.parameters.add @parameter_service_id
316
+ ##~ op.parameters.add @parameter_user_key
317
+ ##~ op.parameters.add @parameter_referrer
318
+ ##~ op.parameters.add @parameter_usage
319
+ ##~ op.parameters.add @parameter_log
320
+ ##
321
+ get '/transactions/authrep.xml' do
322
+ do_api_method :authrep
323
+ end
324
+
325
+ ## ------------ DOCS --------------
326
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
327
+ ##~ sapi = source2swagger.namespace(namespace)
328
+ ##~ a = sapi.apis.add
329
+ ##~ a.set "path" => "/transactions/oauth_authrep.xml", "format" => "xml"
330
+ ##~ op = a.operations.add
331
+ ##~ op.set :httpMethod => "GET", :nickname => "oauth_authrep", :deprecated => false
332
+ ##~ op.summary = "AuthRep (OAuth authentication mode pattern)"
333
+ ##
334
+ ##~ op.description = "<p>Authrep is a <b>'one-shot'</b> operation to authorize an application and report the associated transaction at the same time in the OAuth authentication pattern."
335
+ ##~ op.description = op.description + @authrep_desc + @oauth_security + @oauth_desc_response
336
+ ##~ op.group = "authrep"
337
+ ##
338
+ ##~ op.parameters.add @parameter_service_token
339
+ ##~ op.parameters.add @parameter_service_id
340
+ ##~ op.parameters.add @parameter_access_token
341
+ ##~ op.parameters.add @parameter_client_id
342
+ ##~ op.parameters.add @parameter_app_key_oauth
343
+ ##~ op.parameters.add @parameter_referrer
344
+ ##~ op.parameters.add @parameter_usage
345
+ ##~ op.parameters.add @parameter_log
346
+ ##~ op.parameters.add @parameter_redirect_url
347
+ ##~ op.parameters.add @parameter_redirect_uri
348
+ ##
349
+ get '/transactions/oauth_authrep.xml' do
350
+ do_api_method :oauth_authrep
351
+ end
352
+
353
+ ## ------------ DOCS --------------
354
+ ##~ namespace = ENV['SAAS_SWAGGER'] == "1" ? "Service Management API" : "Service Management API (on-premises)"
355
+ ##~ sapi = source2swagger.namespace(namespace)
356
+ ##~ a = sapi.apis.add
357
+ ##~ a.set "path" => "/transactions.xml", "format" => "xml"
358
+ ##~ op = a.operations.add
359
+ ##~ op.set :httpMethod => "POST"
360
+ ##~ op.summary = "Report (App Id authentication pattern)"
361
+
362
+ ##~ @post_notes = "<p>Supported <code class='http'>Content-Type</code> values for this POST call are: <code class='http'>application/x-www-form-urlencoded</code>."
363
+ ##~ @report_desc = "<p>Report the transactions to 3scale backend.<p>This operation updates the metrics passed in the usage parameter. You can send up to 1K"
364
+ ##~ @report_desc = @report_desc + " transactions in a single POST request. Transactions are processed asynchronously by the 3scale's backend."
365
+ ##~ @report_desc = @report_desc + "<p>Transactions from a single batch are reported only if all of them are valid. If there is an error in"
366
+ ##~ @report_desc = @report_desc + " processing of at least one of them, none is reported.<p>Note that a batch can only report transactions to the same"
367
+ ##~ @report_desc = @report_desc + " service, <em>service_id</em> is at the same level that <em>service_token</em>. Multiple report calls will have to be issued to report"
368
+ ##~ @report_desc = @report_desc + " transactions to different services."
369
+ ##~ @report_desc = @report_desc + "<p>Be aware that reporting metrics that are limited at the time of reporting will have no effect."
370
+ ##~ @report_desc = @report_desc + @post_notes
371
+ ##
372
+ ##~ op.description = @report_desc
373
+ ##~ op.group = "report"
374
+ #
375
+ ##~ op.parameters.add @parameter_service_token
376
+ ##~ op.parameters.add @parameter_service_id
377
+ ##~ op.parameters.add @parameter_transaction_app_id
378
+ ##
379
+ ##~ a = sapi.apis.add
380
+ ##~ a.set "path" => "/transactions.xml", "format" => "xml"
381
+ ##~ op = a.operations.add
382
+ ##~ op.set :httpMethod => "POST"
383
+ ##~ op.summary = "Report (API Key authentication pattern)"
384
+ ##~ op.description = @report_desc
385
+ ##~ op.group = "report"
386
+ #
387
+ ##~ op.parameters.add @parameter_service_token
388
+ ##~ op.parameters.add @parameter_service_id
389
+ ##~ op.parameters.add @parameter_transaction_api_key
390
+ ##
391
+ ##~ a = sapi.apis.add
392
+ ##~ a.set "path" => "/transactions.xml", "format" => "xml"
393
+ ##~ op = a.operations.add
394
+ ##~ op.set :httpMethod => "POST"
395
+ ##~ op.summary = "Report (OAuth authentication pattern)"
396
+ ##~ op.description = @report_desc
397
+ ##~ op.group = "report"
398
+ #
399
+ ##~ op.parameters.add @parameter_service_token
400
+ ##~ op.parameters.add @parameter_service_id
401
+ ##~ op.parameters.add @parameter_transaction_oauth
402
+ ##
403
+ ##
404
+ post '/transactions.xml' do
405
+ check_post_content_type!
406
+
407
+ # 403 Forbidden for consistency (but we should return 400 Bad Request)
408
+ if params.nil?
409
+ logger.notify("listener: params hash is nil in method '/transactions.xml'")
410
+ halt 403
411
+ end
412
+
413
+ # returns 403 when no provider key is given, even if other params have an invalid encoding
414
+ provider_key = params[:provider_key] ||
415
+ provider_key_from(params[:service_token], params[:service_id])
416
+
417
+ raise_provider_key_error(params) if blank?(provider_key)
418
+
419
+ # no need to check params key encoding. Sinatra framework does it for us.
420
+ check_params_value_encoding!(params, REPORT_EXPECTED_PARAMS)
421
+
422
+ transactions = params[:transactions]
423
+ check_transactions_validity(transactions)
424
+
425
+ transactions.values.each do |tr|
426
+ log_without_unused_attrs(tr['log']) if tr['log']
427
+ end
428
+
429
+ Transactor.report(provider_key, params[:service_id], transactions, response_code: 202, request: request_info)
430
+ 202
431
+ end
432
+
433
+ ## OAUTH ACCESS TOKENS
434
+
435
+ post '/services/:service_id/oauth_access_tokens.xml' do
436
+ check_post_content_type!
437
+ require_params! :service_id, :token
438
+
439
+ service_id = params[:service_id]
440
+ ensure_authenticated!(params[:provider_key], params[:service_token], service_id)
441
+
442
+ app_id = params[:app_id]
443
+ raise ApplicationNotFound, app_id unless Application.exists?(service_id, app_id)
444
+
445
+ OAuth::Token::Storage.create(params[:token], service_id, app_id, params[:ttl])
446
+ end
447
+
448
+ delete '/services/:service_id/oauth_access_tokens/:token.xml' do
449
+ require_params! :service_id, :token
450
+
451
+ service_id = params[:service_id]
452
+ ensure_authenticated!(params[:provider_key], params[:service_token], service_id)
453
+
454
+ token = params[:token]
455
+
456
+ # TODO: perhaps improve this to list the deleted tokens?
457
+ raise AccessTokenInvalid, token unless OAuth::Token::Storage.delete(token, service_id)
458
+ end
459
+
460
+ get '/services/:service_id/applications/:app_id/oauth_access_tokens.xml' do
461
+ require_params! :service_id, :app_id
462
+
463
+ service_id = params[:service_id]
464
+ ensure_authenticated!(params[:provider_key], params[:service_token], service_id)
465
+
466
+ app_id = params[:app_id]
467
+
468
+ raise ApplicationNotFound, app_id unless Application.exists?(service_id, app_id)
469
+
470
+ @tokens = OAuth::Token::Storage.all_by_service_and_app service_id, app_id
471
+ builder :oauth_access_tokens
472
+ end
473
+
474
+ get '/services/:service_id/oauth_access_tokens/:token.xml' do
475
+ require_params! :service_id, :token
476
+
477
+ service_id = params[:service_id]
478
+ ensure_authenticated!(params[:provider_key], params[:service_token], service_id)
479
+
480
+ @token_to_app_id = OAuth::Token::Storage.get_credentials(params[:token], service_id)
481
+
482
+ builder :oauth_app_id_by_token
483
+ end
484
+
485
+ get '/check.txt' do
486
+ content_type 'text/plain'
487
+ body 'ok'
488
+ end
489
+
490
+ # using a class variable instead of settings because we want this to be
491
+ # as fast as possible when responding, since we hit /status a lot.
492
+ @@status = { status: :ok,
493
+ version: { backend: ThreeScale::Backend::VERSION } }.to_json.freeze
494
+
495
+ get '/status' do
496
+ content_type 'application/json'.freeze
497
+ @@status
498
+ end
499
+
500
+ @@not_found = [404, { 'Content-Type' => 'application/vnd.3scale-v2.0+xml' }, ['']].freeze
501
+
502
+ not_found do
503
+ env['sinatra.error'.freeze] = nil
504
+ @@not_found
505
+ end
506
+
507
+ private
508
+
509
+ def blank?(object)
510
+ !object || object.respond_to?(:empty?) && object.empty?
511
+ end
512
+
513
+ def valid_usage_params?
514
+ params[:usage].nil? || params[:usage].is_a?(Hash)
515
+ end
516
+
517
+ def require_params!(*keys)
518
+ raise RequiredParamsMissing unless params && keys.all? { |key| !blank?(params[key]) }
519
+ end
520
+
521
+ def check_params_value_encoding!(input_params, params_to_validate)
522
+ params_to_validate.each do |p|
523
+ param_value = input_params[p]
524
+ if !param_value.nil? && !param_value.valid_encoding?
525
+ halt 400, ThreeScale::Backend::NotValidData.new.to_xml
526
+ end
527
+ end
528
+ end
529
+
530
+ def normalize_non_empty_keys!
531
+ AUTH_AUTHREP_COMMON_PARAMS.each do |p|
532
+ thisparam = params[p]
533
+ if !thisparam.nil?
534
+ if thisparam.class != String
535
+ params[p] = nil
536
+ else
537
+ unless thisparam.valid_encoding?
538
+ halt 400, ThreeScale::Backend::NotValidData.new.to_xml
539
+ end
540
+ contents = thisparam.strip
541
+ # Unfortunately some users send empty app_keys that should have
542
+ # been populated for some OAuth flows - this poses a problem
543
+ # because app_key must be kept even if empty if it exists as it is
544
+ # semantically different for authorization endpoints (ie. it
545
+ # forces authentication to happen).
546
+ if p == 'app_key'.freeze
547
+ params[p] = contents
548
+ else
549
+ params[p] = nil if contents.empty?
550
+ end
551
+ end
552
+ end
553
+ end
554
+ end
555
+
556
+ def invalid_post_content_type?(content_type)
557
+ content_type && !content_type.empty? &&
558
+ content_type != 'application/x-www-form-urlencoded'.freeze &&
559
+ content_type != 'multipart/form-data'.freeze
560
+ end
561
+
562
+ def check_post_content_type!
563
+ ctype = request.media_type
564
+ raise ContentTypeInvalid, ctype if invalid_post_content_type?(ctype)
565
+ end
566
+
567
+ def check_transactions_validity(transactions)
568
+ if blank?(transactions)
569
+ raise TransactionsIsBlank
570
+ end
571
+
572
+ if !transactions.is_a?(Hash)
573
+ raise TransactionsFormatInvalid
574
+ end
575
+
576
+ if transactions.any? { |_id, data| data.nil? }
577
+ raise TransactionsHasNilTransaction
578
+ end
579
+
580
+ if transactions.any? { |_id, data| data.is_a?(Hash) && data[:user_id] }
581
+ raise EndUsersNoLongerSupported
582
+ end
583
+ end
584
+
585
+ # In the past, the log field in a transaction could also include
586
+ # "response" and "request". Those fields are not used anymore, but some
587
+ # callers are still sending them. We want to filter them to avoid storing
588
+ # them in the job queues, decoding them, etc. unnecessarily.
589
+ def log_without_unused_attrs(log)
590
+ log.select! { |k| k == 'code' }
591
+ end
592
+
593
+ # In previous versions it was possible to authorize by end-user.
594
+ # Apisonator used the "user_id" param to do that.
595
+ # That's no longer supported, and we want to raise an error when we
596
+ # detect that param to let the user know that.
597
+ def check_no_user_id
598
+ if params && params[:user_id]
599
+ raise EndUsersNoLongerSupported
600
+ end
601
+ end
602
+
603
+ def application
604
+ @application ||= Application.load_by_id_or_user_key!(service_id, params[:app_id], params[:user_key])
605
+ end
606
+
607
+ def service_id
608
+ if params[:service_id].nil? || params[:service_id].empty?
609
+ @service_id ||= Service.default_id!(params[:provider_key])
610
+ else
611
+ unless Service.authenticate_service_id(params[:service_id], params[:provider_key])
612
+ raise ProviderKeyInvalid, params[:provider_key]
613
+ end
614
+ @service_id ||= params[:service_id]
615
+ end
616
+ end
617
+
618
+ def request_info
619
+ {
620
+ url: request.url,
621
+ method: request.request_method,
622
+ form_vars: request.env["rack.request.form_vars"],
623
+ user_agent: request.user_agent,
624
+ ip: request.ip,
625
+ content_type: request.content_type,
626
+ content_length: request.content_length,
627
+ extensions: threescale_extensions,
628
+ }
629
+ end
630
+
631
+ def provider_key_from(service_token, service_id)
632
+ if blank?(service_token) ||
633
+ blank?(service_id) ||
634
+ !ServiceToken.exists?(service_token, service_id)
635
+ nil
636
+ else
637
+ Service.provider_key_for(service_id)
638
+ end
639
+ end
640
+
641
+ # Raises the appropriate error when provider key is blank.
642
+ # Provider key is blank only when these 2 conditions are met:
643
+ # 1) It is not received by parameter (params[:provider_key] is nil)
644
+ # 2) It cannot be obtained using a service token and a service ID.
645
+ # This can happen when these 2 are not received or when the pair is
646
+ # not associated with a provider key.
647
+ def raise_provider_key_error(params)
648
+ token, id = params[:service_token], params[:service_id]
649
+ raise ProviderKeyOrServiceTokenRequired if blank?(token)
650
+ raise ServiceIdMissing if blank?(id)
651
+ raise ServiceTokenInvalid.new(token, id)
652
+ end
653
+
654
+ def ensure_authenticated!(provider_key, service_token, service_id)
655
+ if blank?(provider_key)
656
+ key = provider_key_from(service_token, service_id)
657
+ raise_provider_key_error(params) if blank?(key)
658
+ elsif !Service.authenticate_service_id(service_id, provider_key)
659
+ raise ProviderKeyInvalid, provider_key
660
+ end
661
+ end
662
+
663
+ def response_auth_call(auth_status)
664
+ status(auth_status.authorized? ? 200 : 409)
665
+ optionally_set_headers(auth_status)
666
+ body(threescale_extensions[:no_body] ? nil : auth_status.to_xml)
667
+ end
668
+
669
+ def optionally_set_headers(auth_status)
670
+ set_rejection_reason_header(auth_status)
671
+ set_limit_headers(auth_status)
672
+ end
673
+
674
+ def set_rejection_reason_header(auth_status)
675
+ if !auth_status.authorized? &&
676
+ threescale_extensions[:rejection_reason_header] == '1'.freeze
677
+ response['3scale-rejection-reason'.freeze] = auth_status.rejection_reason_code
678
+ end
679
+ end
680
+
681
+ def set_limit_headers(auth_status)
682
+ if threescale_extensions[:limit_headers] == '1'.freeze &&
683
+ (auth_status.authorized? || auth_status.rejection_reason_code == LimitsExceeded.code)
684
+ auth_status.limit_headers.each do |hdr, value|
685
+ response["3scale-limit-#{hdr}"] = value
686
+ end
687
+ end
688
+ end
689
+
690
+ def threescale_extensions
691
+ @threescale_extensions ||= self.class.threescale_extensions request.env, params
692
+ end
693
+
694
+ # Listener.threescale_extensions - this is a public class method
695
+ #
696
+ # Collect 3scale extensions or optional features.
697
+ def self.threescale_extensions(env, params = nil)
698
+ options = env['HTTP_3SCALE_OPTIONS'.freeze]
699
+ if options
700
+ ::Rack::Utils.parse_nested_query(options).symbolize_names
701
+ else
702
+ {}
703
+ end.tap do |ext|
704
+ # no_body must be supported from URL params, as it has users
705
+ no_body = ext[:no_body] || deprecated_no_body_param(env, params)
706
+ # This particular param was expected to be specified (no matter the
707
+ # value) or having the string 'true' as value. We are going to
708
+ # accept any value except '0' or 'false'.
709
+ if no_body
710
+ ext[:no_body] = no_body != 'false' && no_body != '0'
711
+ end
712
+ end
713
+ end
714
+
715
+ def self.deprecated_no_body_param(env, params)
716
+ if params.nil?
717
+ # check the request parameters from the Rack environment
718
+ qh = env['rack.request.query_hash'.freeze]
719
+ qh['no_body'.freeze] unless qh.nil?
720
+ else
721
+ params[:no_body]
722
+ end
723
+ end
724
+
725
+ private_class_method :deprecated_no_body_param
726
+ end
727
+ end
728
+ end