apisonator 3.3.2 → 3.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 881dcbb24dbe3757ed8d786f49cdf6aab9dce3bb060f26387f142b4e825e06a1
4
- data.tar.gz: e25f370546c286d5400fdbf19331ef554f34ce47c91df522007039c1fbf322e4
3
+ metadata.gz: bb8035c96211d326fdd9231d4f7d92303ca0821d813cc01b2e44bef5c9ec8f4b
4
+ data.tar.gz: 3fb050d336b0bc774281741590a7b848c63a2ef05300ede3602907fb116b48d0
5
5
  SHA512:
6
- metadata.gz: 618be1a317cee54f1892357f7a64739b79eb0e203e15ae03a367176642e4d526c72d283a59438640e1e6503fd5e72a85be230deab76e6ac13c2305e8080aacd5
7
- data.tar.gz: e7150ec528c2dc2b2b0f53a69fa50b2f5b7af52f736ca0a74196a38ae6be1b8a6913fd81fc609f00e18278c2fab571a7a926f647635b9b264b7abd7099673584
6
+ metadata.gz: a7fd9acbfaf34c91682cce1fe47014dfcca36895e5520970fcc1b9a7c2d583ee2c467c21c87a41328d1c7331a192b104324a403a5888a55cfc22a7835e646ee4
7
+ data.tar.gz: 74004a2879c25b1cbab73369f5d261e64eac2ed0ce218758d4605322361eae88058caf2f512fbe993d98e6958251ecb55305171e5f93e2178131a75a5ee8157d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,59 @@
2
2
 
3
3
  Notable changes to Apisonator will be tracked in this document.
4
4
 
5
+ ## 3.4.3 - 2021-06-23
6
+
7
+ ### Fixed
8
+
9
+ - Fixed bug that causes Apisonator to stop pinging Porta to fetch events. This
10
+ bug triggers very rarely
11
+ ([#292](https://github.com/3scale/apisonator/pull/292)).
12
+
13
+ ## 3.4.2 - 2021-06-17
14
+
15
+ ### Fixed
16
+
17
+ - Fixed exception raised when TTL = 0 in `UsagesChecked.mark_all_checked`
18
+ ([#290](https://github.com/3scale/apisonator/pull/290)).
19
+
20
+ ## 3.4.1 - 2021-06-16
21
+
22
+ ### Changed
23
+
24
+ - Introduced performance optimizations around alerts detection
25
+ ([#287](https://github.com/3scale/apisonator/pull/287)).
26
+
27
+ ## 3.4.0 - 2021-06-14
28
+
29
+ ### Added
30
+
31
+ - New extension that list the keys of an application
32
+ ([#284](https://github.com/3scale/apisonator/pull/284)).
33
+
34
+ ### Changed
35
+
36
+ - It is now possible to use OIDC in the auth and authrep endpoints
37
+ ([#280](https://github.com/3scale/apisonator/pull/280)).
38
+ - Updated multi-json to 1.15.0
39
+ ([#278](https://github.com/3scale/apisonator/pull/278)).
40
+
41
+ ### Removed
42
+
43
+ - Deleted unused service attributes related with deleted end-users functionality
44
+ ([#277](https://github.com/3scale/apisonator/pull/277)).
45
+
46
+ ## 3.3.3 - 2021-03-09
47
+
48
+ ### Changed
49
+
50
+ - Check if alerts can be raised before calculating utilization (perf
51
+ optimization) ([#275](https://github.com/3scale/apisonator/pull/275)).
52
+
53
+ ### Removed
54
+
55
+ - Stop maintaining unused "current_max" key in Alerts
56
+ ([#272](https://github.com/3scale/apisonator/pull/272)).
57
+
5
58
  ## 3.3.2 - 2021-02-23
6
59
 
7
60
  ### Fixed
data/Gemfile.lock CHANGED
@@ -36,7 +36,7 @@ GIT
36
36
  PATH
37
37
  remote: .
38
38
  specs:
39
- apisonator (3.3.2)
39
+ apisonator (3.4.3)
40
40
 
41
41
  GEM
42
42
  remote: https://rubygems.org/
@@ -137,7 +137,7 @@ GEM
137
137
  mocha (1.3.0)
138
138
  metaclass (~> 0.0.1)
139
139
  mono_logger (1.1.0)
140
- multi_json (1.13.1)
140
+ multi_json (1.15.0)
141
141
  mustache (1.0.5)
142
142
  mustermann (1.0.2)
143
143
  net-scp (1.2.1)
data/Gemfile.on_prem.lock CHANGED
@@ -36,7 +36,7 @@ GIT
36
36
  PATH
37
37
  remote: .
38
38
  specs:
39
- apisonator (3.3.2)
39
+ apisonator (3.4.3)
40
40
 
41
41
  GEM
42
42
  remote: https://rubygems.org/
@@ -126,7 +126,7 @@ GEM
126
126
  mocha (1.3.0)
127
127
  metaclass (~> 0.0.1)
128
128
  mono_logger (1.1.0)
129
- multi_json (1.13.1)
129
+ multi_json (1.15.0)
130
130
  mustache (1.0.5)
131
131
  mustermann (1.0.2)
132
132
  net-scp (1.2.1)
@@ -44,6 +44,7 @@ require '3scale/backend/queue_storage'
44
44
  require '3scale/backend/errors'
45
45
  require '3scale/backend/stats'
46
46
  require '3scale/backend/usage_limit'
47
+ require '3scale/backend/utilization'
47
48
  require '3scale/backend/alerts'
48
49
  require '3scale/backend/event_storage'
49
50
  require '3scale/backend/worker'
@@ -1,21 +1,19 @@
1
1
  module ThreeScale
2
2
  module Backend
3
3
  class AlertLimit
4
- module KeyHelpers
5
- def key(service_id)
6
- "alerts/service_id:#{service_id}/allowed_set"
7
- end
8
- end
9
-
10
- include KeyHelpers
11
- extend KeyHelpers
4
+ include Alerts::KeyHelpers
5
+ extend Alerts::KeyHelpers
12
6
 
13
7
  include Storable
14
8
 
15
9
  attr_accessor :service_id, :value
16
10
 
17
11
  def save
18
- storage.sadd(key(service_id), value.to_i) if valid?
12
+ if valid?
13
+ res = storage.sadd(key_allowed_set(service_id), value.to_i)
14
+ Alerts::UsagesChecked.invalidate_for_service(service_id)
15
+ res
16
+ end
19
17
  end
20
18
 
21
19
  def to_hash
@@ -26,7 +24,7 @@ module ThreeScale
26
24
  end
27
25
 
28
26
  def self.load_all(service_id)
29
- values = storage.smembers(key(service_id))
27
+ values = storage.smembers(key_allowed_set(service_id))
30
28
  values.map do |value|
31
29
  new(service_id: service_id, value: value.to_i)
32
30
  end
@@ -38,7 +36,7 @@ module ThreeScale
38
36
  end
39
37
 
40
38
  def self.delete(service_id, value)
41
- storage.srem(key(service_id), value.to_i) if valid_value?(value)
39
+ storage.srem(key_allowed_set(service_id), value.to_i) if valid_value?(value)
42
40
  end
43
41
 
44
42
  def self.valid_value?(value)
@@ -6,11 +6,10 @@ module ThreeScale
6
6
 
7
7
  # The compacted hour in the params refers to the
8
8
  # TimeHacks.to_compact_s method.
9
- def alert_keys(service_id, app_id, discrete_utilization, compacted_hour_start)
9
+ def alert_keys(service_id, app_id, discrete_utilization)
10
10
  {
11
11
  already_notified: key_already_notified(service_id, app_id, discrete_utilization),
12
12
  allowed: key_allowed_set(service_id),
13
- current_max: key_current_max(service_id, app_id, compacted_hour_start),
14
13
  current_id: key_current_id
15
14
  }
16
15
  end
@@ -31,18 +30,19 @@ module ThreeScale
31
30
  "#{prefix}allowed_set"
32
31
  end
33
32
 
34
- def key_current_max(service_id, app_id, compacted_hour_start)
35
- prefix = key_prefix(service_id, app_id)
36
- "#{prefix}#{compacted_hour_start}/current_max"
37
- end
38
-
39
33
  def key_current_id
40
34
  'alerts/current_id'.freeze
41
35
  end
36
+
37
+ def key_usage_already_checked(service_id, app_id)
38
+ prefix = key_prefix(service_id, app_id)
39
+ "#{prefix}usage_already_checked"
40
+ end
42
41
  end
43
42
 
44
43
  extend self
45
44
  extend KeyHelpers
45
+ include Memoizer::Decorator
46
46
 
47
47
  ALERT_TTL = 24*3600 # 1 day (only one message per day)
48
48
  ## zero must be here and sorted, yes or yes
@@ -50,67 +50,109 @@ module ThreeScale
50
50
  FIRST_ALERT_BIN = ALERT_BINS.first
51
51
  RALERT_BINS = ALERT_BINS.reverse.freeze
52
52
 
53
- def utilization(app_usage_reports)
54
- max_utilization = -1.0
55
- max_record = nil
56
- max = proc do |item|
57
- if item.max_value > 0
58
- utilization = item.current_value / item.max_value.to_f
59
-
60
- if utilization > max_utilization
61
- max_record = item
62
- max_utilization = utilization
63
- end
53
+ # This class is useful to reduce the amount of information that we need to
54
+ # fetch from Redis to determine whether an alert should be raised.
55
+ # In summary, alerts are raised at the application level and we need to
56
+ # wait for 24h before raising a new one for the same level (ALERTS_BIN
57
+ # above).
58
+ #
59
+ # This class allows us to check all the usage limits once and then not
60
+ # check all of them again (just the ones in the report job) until:
61
+ # 1) A specific alert has expired (24h passed since it was triggered).
62
+ # 2) A new alert bin is enabled for the service.
63
+ class UsagesChecked
64
+ extend KeyHelpers
65
+ extend StorageHelpers
66
+ include Memoizer::Decorator
67
+
68
+ def self.need_to_check_all?(service_id, app_id)
69
+ !storage.exists(key_usage_already_checked(service_id, app_id))
70
+ end
71
+ memoize :need_to_check_all?
72
+
73
+ def self.mark_all_checked(service_id, app_id)
74
+ ttl = ALERT_BINS.map do |bin|
75
+ ttl = storage.ttl(key_already_notified(service_id, app_id, bin))
76
+
77
+ # Redis returns -2 if key does not exist, and -1 if it exists without
78
+ # a TTL (we know this should not happen for the alert bins).
79
+ # In those cases we should just set the TTL to the max (ALERT_TTL).
80
+ ttl >= 0 ? ttl : ALERT_TTL
81
+ end.min
82
+
83
+ # Setex fails when ttl = 0. Also, if it's 0, we don't need to mark it
84
+ # as checked, because the "already_notified" key for the bin is just
85
+ # about to expire, so we'll need to check all the usages.
86
+ if ttl > 0
87
+ storage.setex(key_usage_already_checked(service_id, app_id), ttl, '1'.freeze)
88
+ Memoizer.clear(Memoizer.build_key(self, :need_to_check_all?, service_id, app_id))
64
89
  end
65
90
  end
66
91
 
67
- app_usage_reports.each(&max)
92
+ def self.invalidate(service_id, app_id)
93
+ storage.del(key_usage_already_checked(service_id, app_id))
94
+ end
95
+
96
+ def self.invalidate_for_service(service_id)
97
+ app_ids = []
98
+ cursor = 0
99
+
100
+ loop do
101
+ cursor, ids = storage.sscan(
102
+ Application.applications_set_key(service_id), cursor, count: SCAN_SLICE
103
+ )
104
+
105
+ app_ids += ids
68
106
 
69
- if max_utilization == -1
70
- ## case that all the limits have max_value==0
71
- max_utilization = 0
72
- max_record = app_usage_reports.first
107
+ break if cursor.to_i == 0
108
+ end
109
+
110
+ invalidate_batch(service_id, app_ids)
73
111
  end
74
112
 
75
- [max_utilization, max_record]
113
+ def self.invalidate_batch(service_id, app_ids)
114
+ app_ids.each_slice(PIPELINED_SLICE_SIZE) do |ids|
115
+ keys = ids.map { |app_id| key_usage_already_checked(service_id, app_id) }
116
+ storage.del(keys)
117
+ end
118
+ end
119
+ private_class_method :invalidate_batch
76
120
  end
77
121
 
78
- def update_utilization(service_id, app_id, max_utilization, max_record, timestamp)
79
- discrete = utilization_discrete(max_utilization)
80
- max_utilization_i = (max_utilization * 100.0).round
122
+ def can_raise_more_alerts?(service_id, app_id)
123
+ allowed_bins = allowed_set_for_service(service_id).sort
124
+
125
+ return false if allowed_bins.empty?
81
126
 
82
- beginning_of_day = Period::Boundary.day_start(timestamp)
83
- period_hour = Period::Boundary.hour_start(timestamp).to_compact_s
84
- # UNIX timestamp for key expiration - add 1 day + 5 mins
85
- expire_at = (beginning_of_day + 86700).to_i
127
+ # If the bin with the highest value has already been notified, there's
128
+ # no need to notify anything else.
129
+ not notified?(service_id, app_id, allowed_bins.last)
130
+ end
86
131
 
87
- keys = alert_keys(service_id, app_id, discrete, period_hour)
132
+ def update_utilization(service_id, app_id, utilization)
133
+ discrete = utilization_discrete(utilization.ratio)
88
134
 
89
- already_alerted, allowed, current_max, _ = storage.pipelined do
135
+ keys = alert_keys(service_id, app_id, discrete)
136
+
137
+ already_alerted, allowed = storage.pipelined do
90
138
  storage.get(keys[:already_notified])
91
139
  storage.sismember(keys[:allowed], discrete)
92
- storage.get(keys[:current_max])
93
- storage.expireat(keys[:current_max], expire_at)
94
- end
95
-
96
- ## update the status of utilization
97
- if max_utilization_i > current_max.to_i
98
- storage.set(keys[:current_max], max_utilization_i)
99
140
  end
100
141
 
101
142
  if already_alerted.nil? && allowed && discrete.to_i > 0
102
- next_id, _ = storage.pipelined do
143
+ next_id, _, _ = storage.pipelined do
103
144
  storage.incr(keys[:current_id])
104
145
  storage.setex(keys[:already_notified], ALERT_TTL, "1")
146
+ UsagesChecked.invalidate(service_id, app_id)
105
147
  end
106
148
 
107
149
  alert = { :id => next_id,
108
150
  :utilization => discrete,
109
- :max_utilization => max_utilization,
151
+ :max_utilization => utilization.ratio,
110
152
  :application_id => app_id,
111
153
  :service_id => service_id,
112
- :timestamp => timestamp,
113
- :limit => formatted_limit(max_record) }
154
+ :timestamp => Time.now.utc,
155
+ :limit => utilization.to_s }
114
156
 
115
157
  Backend::EventStorage::store(:alert, alert)
116
158
  end
@@ -124,10 +166,15 @@ module ThreeScale
124
166
  end || FIRST_ALERT_BIN
125
167
  end
126
168
 
127
- def formatted_limit(record)
128
- "#{record.metric_name} per #{record.period}: "\
129
- "#{record.current_value}/#{record.max_value}"
169
+ def allowed_set_for_service(service_id)
170
+ storage.smembers(key_allowed_set(service_id)).map(&:to_i) # Redis returns strings always
171
+ end
172
+ memoize :allowed_set_for_service
173
+
174
+ def notified?(service_id, app_id, bin)
175
+ storage.get(key_already_notified(service_id, app_id, bin))
130
176
  end
177
+ memoize :notified?
131
178
 
132
179
  def storage
133
180
  Storage.instance
@@ -4,6 +4,8 @@ module ThreeScale
4
4
  module All
5
5
  TIME_FORMAT = '%Y-%m-%d %H:%M:%S %z'.freeze
6
6
  PIPELINED_SLICE_SIZE = 400
7
+ SLEEP_BETWEEN_SCANS = 0.01 # In seconds
8
+ SCAN_SLICE = 500
7
9
  end
8
10
 
9
11
  def self.included(base)
@@ -4,7 +4,10 @@ module ThreeScale
4
4
  module Backend
5
5
  class EventStorage
6
6
  PING_TTL = 60
7
- EVENT_TYPES = [:first_traffic, :first_daily_traffic, :alert]
7
+ private_constant :PING_TTL
8
+
9
+ EVENT_TYPES = [:first_traffic, :first_daily_traffic, :alert].freeze
10
+ private_constant :EVENT_TYPES
8
11
 
9
12
  class << self
10
13
  include StorageHelpers
@@ -84,21 +87,15 @@ module ThreeScale
84
87
  URI(events_hook)
85
88
  end
86
89
 
87
- def expire_last_ping
88
- storage.expire(events_ping_key, PING_TTL)
89
- end
90
-
91
90
  def pending_ping?
92
91
  ## the queue is not empty and more than timeout has passed
93
92
  ## since the front-end was notified
94
- events_set_size, ping_key_value = storage.pipelined do
95
- storage.zcard(events_queue_key)
96
- storage.incr(events_ping_key)
93
+ events_set_size, can_ping = storage.pipelined do
94
+ size
95
+ storage.set(events_ping_key, '1'.freeze, ex: PING_TTL, nx: true)
97
96
  end
98
97
 
99
- return false unless ping_key_value.to_i == 1
100
- expire_last_ping
101
- events_set_size > 0
98
+ can_ping && events_set_size > 0
102
99
  end
103
100
 
104
101
  def decode_event(raw_event)
@@ -4,16 +4,12 @@ module ThreeScale
4
4
  include Storable
5
5
 
6
6
  # list of attributes to be fetched from storage
7
- ATTRIBUTES = %i[state referrer_filters_required backend_version
8
- user_registration_required default_user_plan_id
9
- default_user_plan_name provider_key].freeze
7
+ ATTRIBUTES = %i[state referrer_filters_required backend_version provider_key].freeze
10
8
  private_constant :ATTRIBUTES
11
9
 
12
10
  attr_reader :state
13
- attr_accessor :provider_key, :id, :backend_version,
14
- :default_user_plan_id, :default_user_plan_name
15
- attr_writer :referrer_filters_required, :user_registration_required,
16
- :default_service
11
+ attr_accessor :provider_key, :id, :backend_version
12
+ attr_writer :referrer_filters_required, :default_service
17
13
 
18
14
  class << self
19
15
  include Memoizer::Decorator
@@ -104,8 +100,6 @@ module ThreeScale
104
100
  memoize :list
105
101
 
106
102
  def save!(attributes = {})
107
- massage_set_user_registration_required attributes
108
-
109
103
  new(attributes).save!
110
104
  end
111
105
 
@@ -139,26 +133,10 @@ module ThreeScale
139
133
  def massage_service_attrs(service_attrs)
140
134
  service_attrs[:referrer_filters_required] =
141
135
  service_attrs[:referrer_filters_required].to_i > 0
142
- service_attrs[:user_registration_required] =
143
- massage_get_user_registration_required(
144
- service_attrs[:user_registration_required])
145
136
 
146
137
  service_attrs
147
138
  end
148
139
 
149
- # nil => true, 1 => true, '1' => true, 0 => false, '0' => false
150
- def massage_get_user_registration_required(value)
151
- value.nil? ? true : value.to_i > 0
152
- end
153
-
154
- def massage_set_user_registration_required(attributes)
155
- if attributes[:user_registration_required].nil?
156
- val = storage.get(storage_key(attributes[:id], :user_registration_required))
157
- attributes[:user_registration_required] =
158
- (!val.nil? && val.to_i == 0) ? false : true
159
- end
160
- end
161
-
162
140
  def get_attr(id, attribute)
163
141
  storage.get(storage_key(id, attribute))
164
142
  end
@@ -195,10 +173,6 @@ module ThreeScale
195
173
  @referrer_filters_required
196
174
  end
197
175
 
198
- def user_registration_required?
199
- @user_registration_required
200
- end
201
-
202
176
  def save!
203
177
  set_as_default_if_needed
204
178
  persist
@@ -227,9 +201,6 @@ module ThreeScale
227
201
  provider_key: provider_key,
228
202
  backend_version: backend_version,
229
203
  referrer_filters_required: referrer_filters_required?,
230
- user_registration_required: user_registration_required?,
231
- default_user_plan_id: default_user_plan_id,
232
- default_user_plan_name: default_user_plan_name,
233
204
  default_service: default_service?
234
205
  }
235
206
  end
@@ -294,9 +265,6 @@ module ThreeScale
294
265
 
295
266
  def persist_attributes
296
267
  persist_attribute :referrer_filters_required, referrer_filters_required? ? 1 : 0
297
- persist_attribute :user_registration_required, user_registration_required? ? 1 : 0
298
- persist_attribute :default_user_plan_id, default_user_plan_id, true
299
- persist_attribute :default_user_plan_name, default_user_plan_name, true
300
268
  persist_attribute :backend_version, backend_version, true
301
269
  persist_attribute :provider_key, provider_key
302
270
  persist_attribute :state, state.to_s if state
@@ -59,7 +59,7 @@ module ThreeScale
59
59
  touched_apps = aggregate(transactions, current_bucket)
60
60
 
61
61
  ApplicationEvents.generate(touched_apps.values)
62
- update_alerts(touched_apps)
62
+ update_alerts(transactions)
63
63
  begin
64
64
  ApplicationEvents.ping
65
65
  rescue ApplicationEvents::PingFailed => e
@@ -137,33 +137,35 @@ module ThreeScale
137
137
  logger.info(MAX_BUCKETS_CREATED_MSG)
138
138
  end
139
139
 
140
- def update_alerts(applications)
141
- current_timestamp = Time.now.getutc
142
-
143
- applications.each do |_appid, values|
144
- service_id = values[:service_id]
145
- application = Backend::Application.load(service_id,
146
- values[:application_id])
147
-
148
- # The app could have been deleted at some point since the job was
149
- # enqueued. No need to update alerts in that case.
150
- next unless application
151
-
152
- application.load_metric_names
153
- usage = Usage.application_usage(application, current_timestamp)
154
- status = Transactor::Status.new(service_id: service_id,
155
- application: application,
156
- values: usage)
157
-
158
- max_utilization, max_record = Alerts.utilization(
159
- status.application_usage_reports)
160
-
161
- if max_utilization >= 0.0
162
- Alerts.update_utilization(service_id,
163
- values[:application_id],
164
- max_utilization,
165
- max_record,
166
- current_timestamp)
140
+ def update_alerts(transactions)
141
+ transactions.group_by { |tx| tx.application_id }.each do |app_id, txs|
142
+ service_id = txs.first.service_id # All the txs of an app belong to the same service
143
+
144
+ # Finding the max utilization can be costly because it involves
145
+ # loading usage limits and current usages. That's why before that,
146
+ # we check if there are any alerts that can be raised.
147
+ next unless Alerts.can_raise_more_alerts?(service_id, app_id)
148
+
149
+ begin
150
+ max_utilization = if Alerts::UsagesChecked.need_to_check_all?(service_id, app_id)
151
+ Utilization.max_in_all_metrics(service_id, app_id).tap do
152
+ Alerts::UsagesChecked.mark_all_checked(service_id, app_id)
153
+ end
154
+ else
155
+ # metrics_ids here includes the metrics
156
+ # explicitly reported plus their parents in
157
+ # the hierarchy.
158
+ metric_ids = txs.map { |tx| tx.usage.keys }.flatten.uniq
159
+ Utilization.max_in_metrics(service_id, app_id, metric_ids)
160
+ end
161
+ rescue ApplicationNotFound
162
+ # The app could have been deleted at some point since the job
163
+ # was enqueued. No need to update alerts in that case.
164
+ next
165
+ end
166
+
167
+ if max_utilization && max_utilization.ratio > 0
168
+ Alerts.update_utilization(service_id, app_id, max_utilization)
167
169
  end
168
170
  end
169
171
  end
@@ -36,12 +36,6 @@ module ThreeScale
36
36
  KEY_SERVICES_TO_DELETE = 'set_with_services_marked_for_deletion'.freeze
37
37
  private_constant :KEY_SERVICES_TO_DELETE
38
38
 
39
- SLEEP_BETWEEN_SCANS = 0.01 # In seconds
40
- private_constant :SLEEP_BETWEEN_SCANS
41
-
42
- SCAN_SLICE = 500
43
- private_constant :SCAN_SLICE
44
-
45
39
  STATS_KEY_PREFIX = 'stats/'.freeze
46
40
  private_constant :STATS_KEY_PREFIX
47
41
 
@@ -61,6 +61,8 @@ module ThreeScale
61
61
 
62
62
  def validate(oauth, provider_key, report_usage, params, request_info)
63
63
  service = Service.load_with_provider_key!(params[:service_id], provider_key)
64
+ oidc_service = !oauth && service.backend_version == 'oauth'.freeze
65
+
64
66
  # service_id cannot be taken from params since it might be missing there
65
67
  service_id = service.id
66
68
 
@@ -70,12 +72,18 @@ module ThreeScale
70
72
  # significant.
71
73
  params[:app_id] = nil if app_id && app_id.empty?
72
74
 
73
- if oauth
74
- raise ApplicationNotFound.new nil if app_id.nil?
75
- validators = Validators::OAUTH_VALIDATORS
76
- else
77
- validators = Validators::VALIDATORS
78
- end
75
+ # While OIDC without an app_id makes little sense, we would break existing
76
+ # behaviour when calling non oauth_auth*.xml endpoints if we returned an
77
+ # error here, so only do this for oauth_auth*.xml endpoints.
78
+ raise ApplicationNotFound.new nil if oauth && app_id.nil?
79
+
80
+ validators = if oidc_service
81
+ Validators::OIDC_VALIDATORS
82
+ elsif oauth
83
+ Validators::OAUTH_VALIDATORS
84
+ else
85
+ Validators::VALIDATORS
86
+ end
79
87
 
80
88
  params[:user_key] = nil if params[:user_key] && params[:user_key].empty?
81
89
  application = Application.load_by_id_or_user_key!(service_id,
@@ -98,8 +106,9 @@ module ThreeScale
98
106
  # hierarchy parameter adds information in the response needed
99
107
  # to derive which limits affect directly or indirectly the
100
108
  # metrics for which authorization is requested.
101
- hierarchy: extensions[:hierarchy] == '1',
102
- flat_usage: extensions[:flat_usage] == '1'
109
+ hierarchy: extensions[:hierarchy] == '1'.freeze,
110
+ flat_usage: extensions[:flat_usage] == '1'.freeze,
111
+ list_app_keys: extensions[:list_app_keys] == '1'.freeze
103
112
  }
104
113
 
105
114
  application.load_metric_names
@@ -108,24 +117,6 @@ module ThreeScale
108
117
  apply_validators(validators, status_attrs, params)
109
118
  end
110
119
 
111
- def get_token_ids(token, service_id, app_id)
112
- begin
113
- token_aid = OAuth::Token::Storage.get_credentials(token, service_id)
114
- rescue AccessTokenInvalid => e
115
- # Yep, well, er. Someone specified that it is OK to have an
116
- # invalid token if an app_id is specified. Somehow passing in
117
- # a user_key is still not enough, though...
118
- raise e if app_id.nil?
119
- end
120
-
121
- # We only take the token ids into account if we had no parameter ids
122
- if app_id.nil?
123
- app_id = token_aid
124
- end
125
-
126
- app_id
127
- end
128
-
129
120
  def do_authorize(method, provider_key, params, context_info)
130
121
  notify_authorize(provider_key)
131
122
  validate(method == :oauth_authorize, provider_key, false, params, context_info[:request])
@@ -8,17 +8,23 @@ module ThreeScale
8
8
  # We only use 'redirect_uri' if a request sent such a param. See #397.
9
9
  REDIRECT_URI_FIELD = 'redirect_url'.freeze
10
10
  private_constant :REDIRECT_URI_FIELD
11
+ # Maximum number of keys to list when using the list_app_keys extension
12
+ # At the time of writing System/Porta has a limit of 5 different app_keys
13
+ # at any given moment, but this could change anytime.
14
+ LIST_APP_KEYS_MAX = 256
15
+ private_constant :LIST_APP_KEYS_MAX
11
16
 
12
17
  def initialize(attributes)
13
- @service_id = attributes[:service_id]
14
- @application = attributes[:application]
15
- @oauth = attributes[:oauth]
16
- @usage = attributes[:usage]
17
- @predicted_usage = attributes[:predicted_usage]
18
- @values = filter_values(attributes[:values] || {})
19
- @timestamp = attributes[:timestamp] || Time.now.getutc
20
- @hierarchy_ext = attributes[:hierarchy]
21
- @flat_usage_ext = attributes[:flat_usage]
18
+ @service_id = attributes[:service_id]
19
+ @application = attributes[:application]
20
+ @oauth = attributes[:oauth]
21
+ @usage = attributes[:usage]
22
+ @predicted_usage = attributes[:predicted_usage]
23
+ @values = filter_values(attributes[:values] || {})
24
+ @timestamp = attributes[:timestamp] || Time.now.getutc
25
+ @hierarchy_ext = attributes[:hierarchy]
26
+ @flat_usage_ext = attributes[:flat_usage]
27
+ @list_app_keys_ext = attributes[:list_app_keys]
22
28
 
23
29
  raise 'service_id not specified' if @service_id.nil?
24
30
  raise ':application is required' if @application.nil?
@@ -106,6 +112,7 @@ module ThreeScale
106
112
  add_plan_section(xml, 'plan'.freeze, plan_name)
107
113
  add_reports_section(xml, application_usage_reports)
108
114
  hierarchy_reports.concat application_usage_reports if hierarchy_reports
115
+ add_app_keys_section xml if @list_app_keys_ext
109
116
  end
110
117
 
111
118
  if hierarchy_reports
@@ -161,6 +168,17 @@ module ThreeScale
161
168
  xml << '</hierarchy>'.freeze
162
169
  end
163
170
 
171
+ def add_app_keys_section(xml)
172
+ xml << '<app_keys app="'.freeze
173
+ xml << @application.id << '" svc="'.freeze
174
+ xml << @service_id << '">'.freeze
175
+ @application.keys.take(LIST_APP_KEYS_MAX).each do |key|
176
+ xml << '<key id="'.freeze
177
+ xml << key << '"/>'.freeze
178
+ end
179
+ xml << '</app_keys>'.freeze
180
+ end
181
+
164
182
  # helper to iterate over reports and get relevant hierarchy info
165
183
  def with_report_and_hierarchy(reports)
166
184
  reports.each do |ur|
@@ -3,7 +3,7 @@ module ThreeScale
3
3
  module Transactor
4
4
  class Status
5
5
  class UsageReport
6
- attr_reader :type, :period
6
+ attr_reader :type, :usage_limit, :period
7
7
 
8
8
  def initialize(status, usage_limit)
9
9
  @status = status
@@ -3,12 +3,19 @@ module ThreeScale
3
3
  class Usage
4
4
  class << self
5
5
  def application_usage(application, timestamp)
6
- usage(application, timestamp) do |metric_id, instance_period|
6
+ usage(application.usage_limits, timestamp) do |metric_id, instance_period|
7
7
  Stats::Keys.application_usage_value_key(
8
8
  application.service_id, application.id, metric_id, instance_period)
9
9
  end
10
10
  end
11
11
 
12
+ def application_usage_for_limits(application, timestamp, usage_limits)
13
+ usage(usage_limits, timestamp) do |metric_id, instance_period|
14
+ Stats::Keys.application_usage_value_key(
15
+ application.service_id, application.id, metric_id, instance_period)
16
+ end
17
+ end
18
+
12
19
  def is_set?(usage_str)
13
20
  usage_str && usage_str[0] == '#'.freeze
14
21
  end
@@ -25,7 +32,7 @@ module ThreeScale
25
32
 
26
33
  private
27
34
 
28
- def usage(obj, timestamp)
35
+ def usage(usage_limits, timestamp)
29
36
  # The timestamp does not change, so we can generate all the
30
37
  # instantiated periods just once.
31
38
  # This is important. Without this, the code can generate many instance
@@ -33,7 +40,7 @@ module ThreeScale
33
40
  # time.
34
41
  instance_periods = Period::instance_periods_for_ts(timestamp)
35
42
 
36
- pairs = metric_period_pairs obj.usage_limits
43
+ pairs = metric_period_pairs usage_limits
37
44
  return {} if pairs.empty?
38
45
 
39
46
  keys = pairs.map do |(metric_id, period)|
@@ -0,0 +1,83 @@
1
+ module ThreeScale
2
+ module Backend
3
+ class Utilization
4
+ include Comparable
5
+
6
+ attr_reader :metric_id, :period, :max_value, :current_value
7
+
8
+ def initialize(limit, current_value)
9
+ @metric_id = limit.metric_id
10
+ @period = limit.period
11
+ @max_value = limit.value
12
+ @current_value = current_value
13
+ @encoded = encoded(limit, current_value)
14
+ end
15
+
16
+ def ratio
17
+ return 0 if max_value == 0 # Disabled metric
18
+ current_value/max_value.to_f
19
+ end
20
+
21
+ # Returns in the format needed by the Alerts class.
22
+ def to_s
23
+ @encoded
24
+ end
25
+
26
+ def <=>(other)
27
+ # Consider "disabled" the lowest ones
28
+ if ratio == 0 && other.ratio == 0
29
+ return max_value <=> other.max_value
30
+ end
31
+
32
+ ratio <=> other.ratio
33
+ end
34
+
35
+ # Note: this can return nil
36
+ def self.max_in_all_metrics(service_id, app_id)
37
+ application = Backend::Application.load!(service_id, app_id)
38
+
39
+ usage = Usage.application_usage(application, Time.now.getutc)
40
+
41
+ status = Transactor::Status.new(service_id: service_id,
42
+ application: application,
43
+ values: usage)
44
+
45
+ # Preloads all the metric names to avoid fetching them one by one when
46
+ # generating the usage reports
47
+ application.load_metric_names
48
+
49
+ max = status.application_usage_reports.map do |usage_report|
50
+ Utilization.new(usage_report.usage_limit, usage_report.current_value)
51
+ end.max
52
+
53
+ # Avoid returning a utilization for disabled metrics
54
+ max && max.max_value > 0 ? max : nil
55
+ end
56
+
57
+ # Note: this can return nil
58
+ def self.max_in_metrics(service_id, app_id, metric_ids)
59
+ application = Backend::Application.load!(service_id, app_id)
60
+
61
+ limits = UsageLimit.load_for_affecting_metrics(
62
+ service_id, application.plan_id, metric_ids
63
+ )
64
+
65
+ usage = Usage.application_usage_for_limits(application, Time.now.getutc, limits)
66
+
67
+ max = limits.map do |limit|
68
+ Utilization.new(limit, usage[limit.period][limit.metric_id])
69
+ end.max
70
+
71
+ # Avoid returning a utilization for disabled metrics
72
+ max && max.max_value > 0 ? max : nil
73
+ end
74
+
75
+ private
76
+
77
+ def encoded(limit, current_value)
78
+ metric_name = Metric.load_name(limit.service_id, limit.metric_id)
79
+ "#{metric_name} per #{limit.period}: #{current_value}/#{limit.value}"
80
+ end
81
+ end
82
+ end
83
+ end
@@ -21,6 +21,13 @@ module ThreeScale
21
21
  OAUTH_VALIDATORS = ([Validators::OauthSetting,
22
22
  Validators::OauthKey,
23
23
  Validators::RedirectURI] + COMMON_VALIDATORS).freeze
24
+
25
+ # OIDC specific validators will only check app keys when app_key is given.
26
+ #
27
+ # No need to add OauthSetting, since we need to check that to tell
28
+ # OIDC apart from the rest when calling authrep.xml (note lack of
29
+ # the oauth_ prefix).
30
+ OIDC_VALIDATORS = ([Validators::OauthKey] + COMMON_VALIDATORS).freeze
24
31
  end
25
32
  end
26
33
  end
@@ -3,7 +3,7 @@ module ThreeScale
3
3
  module Validators
4
4
  class OauthSetting < Base
5
5
  def apply
6
- if service.backend_version == 'oauth'
6
+ if service.backend_version == 'oauth'.freeze
7
7
  succeed!
8
8
  else
9
9
  fail!(OauthNotEnabled.new)
@@ -1,5 +1,5 @@
1
1
  module ThreeScale
2
2
  module Backend
3
- VERSION = '3.3.2'
3
+ VERSION = '3.4.3'
4
4
  end
5
5
  end
data/licenses.xml CHANGED
@@ -23,7 +23,7 @@
23
23
  </dependency>
24
24
  <dependency>
25
25
  <packageName>apisonator</packageName>
26
- <version>3.3.2</version>
26
+ <version>3.4.3</version>
27
27
  <licenses>
28
28
  <license>
29
29
  <name>Apache 2.0</name>
@@ -475,7 +475,7 @@
475
475
  </dependency>
476
476
  <dependency>
477
477
  <packageName>multi_json</packageName>
478
- <version>1.13.1</version>
478
+ <version>1.15.0</version>
479
479
  <licenses>
480
480
  <license>
481
481
  <name>MIT</name>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: apisonator
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.2
4
+ version: 3.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Ciganek
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2021-02-23 00:00:00.000000000 Z
19
+ date: 2021-06-23 00:00:00.000000000 Z
20
20
  dependencies: []
21
21
  description: This gem provides a daemon that handles authorization and reporting of
22
22
  web services managed by 3scale.
@@ -164,6 +164,7 @@ files:
164
164
  - lib/3scale/backend/usage_limit.rb
165
165
  - lib/3scale/backend/use_cases/provider_key_change_use_case.rb
166
166
  - lib/3scale/backend/util.rb
167
+ - lib/3scale/backend/utilization.rb
167
168
  - lib/3scale/backend/validators.rb
168
169
  - lib/3scale/backend/validators/base.rb
169
170
  - lib/3scale/backend/validators/key.rb