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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +317 -0
- data/Gemfile +11 -0
- data/Gemfile.base +65 -0
- data/Gemfile.lock +319 -0
- data/Gemfile.on_prem +1 -0
- data/Gemfile.on_prem.lock +297 -0
- data/LICENSE +202 -0
- data/NOTICE +15 -0
- data/README.md +230 -0
- data/Rakefile +287 -0
- data/apisonator.gemspec +47 -0
- data/app/api/api.rb +13 -0
- data/app/api/internal/alert_limits.rb +32 -0
- data/app/api/internal/application_keys.rb +49 -0
- data/app/api/internal/application_referrer_filters.rb +43 -0
- data/app/api/internal/applications.rb +77 -0
- data/app/api/internal/errors.rb +54 -0
- data/app/api/internal/events.rb +42 -0
- data/app/api/internal/internal.rb +104 -0
- data/app/api/internal/metrics.rb +40 -0
- data/app/api/internal/service_tokens.rb +46 -0
- data/app/api/internal/services.rb +58 -0
- data/app/api/internal/stats.rb +42 -0
- data/app/api/internal/usagelimits.rb +62 -0
- data/app/api/internal/utilization.rb +23 -0
- data/bin/3scale_backend +223 -0
- data/bin/3scale_backend_worker +26 -0
- data/config.ru +4 -0
- data/config/puma.rb +192 -0
- data/config/schedule.rb +9 -0
- data/ext/mkrf_conf.rb +64 -0
- data/lib/3scale/backend.rb +67 -0
- data/lib/3scale/backend/alert_limit.rb +56 -0
- data/lib/3scale/backend/alerts.rb +137 -0
- data/lib/3scale/backend/analytics/kinesis.rb +3 -0
- data/lib/3scale/backend/analytics/kinesis/adapter.rb +180 -0
- data/lib/3scale/backend/analytics/kinesis/exporter.rb +86 -0
- data/lib/3scale/backend/analytics/kinesis/job.rb +135 -0
- data/lib/3scale/backend/analytics/redshift.rb +3 -0
- data/lib/3scale/backend/analytics/redshift/adapter.rb +367 -0
- data/lib/3scale/backend/analytics/redshift/importer.rb +83 -0
- data/lib/3scale/backend/analytics/redshift/job.rb +33 -0
- data/lib/3scale/backend/application.rb +330 -0
- data/lib/3scale/backend/application_events.rb +76 -0
- data/lib/3scale/backend/background_job.rb +65 -0
- data/lib/3scale/backend/configurable.rb +20 -0
- data/lib/3scale/backend/configuration.rb +151 -0
- data/lib/3scale/backend/configuration/loader.rb +42 -0
- data/lib/3scale/backend/constants.rb +19 -0
- data/lib/3scale/backend/cors.rb +84 -0
- data/lib/3scale/backend/distributed_lock.rb +67 -0
- data/lib/3scale/backend/environment.rb +21 -0
- data/lib/3scale/backend/error_storage.rb +52 -0
- data/lib/3scale/backend/errors.rb +343 -0
- data/lib/3scale/backend/event_storage.rb +120 -0
- data/lib/3scale/backend/experiment.rb +84 -0
- data/lib/3scale/backend/extensions.rb +5 -0
- data/lib/3scale/backend/extensions/array.rb +19 -0
- data/lib/3scale/backend/extensions/hash.rb +26 -0
- data/lib/3scale/backend/extensions/nil_class.rb +13 -0
- data/lib/3scale/backend/extensions/redis.rb +44 -0
- data/lib/3scale/backend/extensions/string.rb +13 -0
- data/lib/3scale/backend/extensions/time.rb +110 -0
- data/lib/3scale/backend/failed_jobs_scheduler.rb +141 -0
- data/lib/3scale/backend/job_fetcher.rb +122 -0
- data/lib/3scale/backend/listener.rb +728 -0
- data/lib/3scale/backend/listener_metrics.rb +99 -0
- data/lib/3scale/backend/logging.rb +48 -0
- data/lib/3scale/backend/logging/external.rb +44 -0
- data/lib/3scale/backend/logging/external/impl.rb +93 -0
- data/lib/3scale/backend/logging/external/impl/airbrake.rb +66 -0
- data/lib/3scale/backend/logging/external/impl/bugsnag.rb +69 -0
- data/lib/3scale/backend/logging/external/impl/default.rb +18 -0
- data/lib/3scale/backend/logging/external/resque.rb +57 -0
- data/lib/3scale/backend/logging/logger.rb +18 -0
- data/lib/3scale/backend/logging/middleware.rb +62 -0
- data/lib/3scale/backend/logging/middleware/json_writer.rb +21 -0
- data/lib/3scale/backend/logging/middleware/text_writer.rb +60 -0
- data/lib/3scale/backend/logging/middleware/writer.rb +143 -0
- data/lib/3scale/backend/logging/worker.rb +107 -0
- data/lib/3scale/backend/manifest.rb +80 -0
- data/lib/3scale/backend/memoizer.rb +277 -0
- data/lib/3scale/backend/metric.rb +275 -0
- data/lib/3scale/backend/metric/collection.rb +91 -0
- data/lib/3scale/backend/oauth.rb +4 -0
- data/lib/3scale/backend/oauth/token.rb +26 -0
- data/lib/3scale/backend/oauth/token_key.rb +30 -0
- data/lib/3scale/backend/oauth/token_storage.rb +313 -0
- data/lib/3scale/backend/oauth/token_value.rb +25 -0
- data/lib/3scale/backend/period.rb +3 -0
- data/lib/3scale/backend/period/boundary.rb +107 -0
- data/lib/3scale/backend/period/cache.rb +28 -0
- data/lib/3scale/backend/period/period.rb +402 -0
- data/lib/3scale/backend/queue_storage.rb +16 -0
- data/lib/3scale/backend/rack.rb +49 -0
- data/lib/3scale/backend/rack/exception_catcher.rb +136 -0
- data/lib/3scale/backend/rack/internal_error_catcher.rb +23 -0
- data/lib/3scale/backend/rack/prometheus.rb +19 -0
- data/lib/3scale/backend/saas.rb +6 -0
- data/lib/3scale/backend/saas_analytics.rb +4 -0
- data/lib/3scale/backend/server.rb +30 -0
- data/lib/3scale/backend/server/falcon.rb +52 -0
- data/lib/3scale/backend/server/puma.rb +71 -0
- data/lib/3scale/backend/service.rb +317 -0
- data/lib/3scale/backend/service_token.rb +97 -0
- data/lib/3scale/backend/stats.rb +8 -0
- data/lib/3scale/backend/stats/aggregator.rb +170 -0
- data/lib/3scale/backend/stats/aggregators/base.rb +72 -0
- data/lib/3scale/backend/stats/aggregators/response_code.rb +58 -0
- data/lib/3scale/backend/stats/aggregators/usage.rb +34 -0
- data/lib/3scale/backend/stats/bucket_reader.rb +135 -0
- data/lib/3scale/backend/stats/bucket_storage.rb +108 -0
- data/lib/3scale/backend/stats/cleaner.rb +195 -0
- data/lib/3scale/backend/stats/codes_commons.rb +14 -0
- data/lib/3scale/backend/stats/delete_job_def.rb +60 -0
- data/lib/3scale/backend/stats/key_generator.rb +73 -0
- data/lib/3scale/backend/stats/keys.rb +104 -0
- data/lib/3scale/backend/stats/partition_eraser_job.rb +58 -0
- data/lib/3scale/backend/stats/partition_generator_job.rb +46 -0
- data/lib/3scale/backend/stats/period_commons.rb +34 -0
- data/lib/3scale/backend/stats/stats_parser.rb +141 -0
- data/lib/3scale/backend/stats/storage.rb +113 -0
- data/lib/3scale/backend/statsd.rb +14 -0
- data/lib/3scale/backend/storable.rb +35 -0
- data/lib/3scale/backend/storage.rb +40 -0
- data/lib/3scale/backend/storage_async.rb +4 -0
- data/lib/3scale/backend/storage_async/async_redis.rb +21 -0
- data/lib/3scale/backend/storage_async/client.rb +205 -0
- data/lib/3scale/backend/storage_async/pipeline.rb +79 -0
- data/lib/3scale/backend/storage_async/resque_extensions.rb +30 -0
- data/lib/3scale/backend/storage_helpers.rb +278 -0
- data/lib/3scale/backend/storage_key_helpers.rb +9 -0
- data/lib/3scale/backend/storage_sync.rb +43 -0
- data/lib/3scale/backend/transaction.rb +62 -0
- data/lib/3scale/backend/transactor.rb +177 -0
- data/lib/3scale/backend/transactor/limit_headers.rb +54 -0
- data/lib/3scale/backend/transactor/notify_batcher.rb +139 -0
- data/lib/3scale/backend/transactor/notify_job.rb +47 -0
- data/lib/3scale/backend/transactor/process_job.rb +33 -0
- data/lib/3scale/backend/transactor/report_job.rb +84 -0
- data/lib/3scale/backend/transactor/status.rb +236 -0
- data/lib/3scale/backend/transactor/usage_report.rb +182 -0
- data/lib/3scale/backend/usage.rb +63 -0
- data/lib/3scale/backend/usage_limit.rb +115 -0
- data/lib/3scale/backend/use_cases/provider_key_change_use_case.rb +60 -0
- data/lib/3scale/backend/util.rb +17 -0
- data/lib/3scale/backend/validators.rb +26 -0
- data/lib/3scale/backend/validators/base.rb +36 -0
- data/lib/3scale/backend/validators/key.rb +17 -0
- data/lib/3scale/backend/validators/limits.rb +57 -0
- data/lib/3scale/backend/validators/oauth_key.rb +15 -0
- data/lib/3scale/backend/validators/oauth_setting.rb +15 -0
- data/lib/3scale/backend/validators/redirect_uri.rb +33 -0
- data/lib/3scale/backend/validators/referrer.rb +60 -0
- data/lib/3scale/backend/validators/service_state.rb +15 -0
- data/lib/3scale/backend/validators/state.rb +15 -0
- data/lib/3scale/backend/version.rb +5 -0
- data/lib/3scale/backend/views/oauth_access_tokens.builder +14 -0
- data/lib/3scale/backend/views/oauth_app_id_by_token.builder +4 -0
- data/lib/3scale/backend/worker.rb +87 -0
- data/lib/3scale/backend/worker_async.rb +88 -0
- data/lib/3scale/backend/worker_metrics.rb +44 -0
- data/lib/3scale/backend/worker_sync.rb +32 -0
- data/lib/3scale/bundler_shim.rb +17 -0
- data/lib/3scale/prometheus_server.rb +10 -0
- data/lib/3scale/tasks/connectivity.rake +41 -0
- data/lib/3scale/tasks/helpers.rb +3 -0
- data/lib/3scale/tasks/helpers/environment.rb +23 -0
- data/lib/3scale/tasks/stats.rake +131 -0
- data/lib/3scale/tasks/swagger.rake +46 -0
- data/licenses.xml +1215 -0
- 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
|