insights-api-common 3.7.0 → 3.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 81867b9d7ef11af5730e42563f88c156e7f51c2ef9e474990942f68535483fee
4
- data.tar.gz: bba675752b9788c7fff6e4375b2301217a063f291713d32694a49227c8bed981
3
+ metadata.gz: 7d2889f1fb8a92c64f95de6f5d4a97f111d35e796a2ab4fb6a6b54ade5bbef05
4
+ data.tar.gz: 951defb8fb43a566b3f16b49e76f340d95d68f839831c1d6896b72c91dd0428d
5
5
  SHA512:
6
- metadata.gz: 40c3eab1a34663c42f8d85416d5b30d32d2066235dbe7bf86992860aa6a8ebd551b0c800ec80b0d8b82ef69e17707a79e2eeed4737205f678fe0c35fe41489ee
7
- data.tar.gz: fc3a7e98ca9a116e94347ba8a09892628299c37571fa940063f5ddd95915ffc633b76170c4b738943c0ab3c4f4484f45f790e68a19494339b0876a3426232202
6
+ metadata.gz: 37ea90782a5dad4f376bcc9b99f051394e855b02f4689c93c8049f7a292cf373da4e4f20f8730efd4f8830e6a680b14e61849c2bf3c5198104b5efb9c167b9ef
7
+ data.tar.gz: 8d331fdbfa57116010b32374e8dfc0ef016249336db7e86dc40d5f685f2f382ff2277064aea57dc9eca451e8be1248f40039828f97762358f3d0d7f9c121f9ed
@@ -90,7 +90,11 @@ module Insights
90
90
 
91
91
  def filtered
92
92
  check_if_openapi_enabled
93
- Insights::API::Common::Filter.new(model, safe_params_for_list[:filter], api_doc_definition).apply
93
+ Insights::API::Common::Filter.new(model, safe_params_for_list[:filter], api_doc_definition, extra_attributes_for_filtering).apply
94
+ end
95
+
96
+ def extra_attributes_for_filtering
97
+ {}
94
98
  end
95
99
 
96
100
  def pagination_limit
@@ -5,13 +5,30 @@ module Insights
5
5
  INTEGER_COMPARISON_KEYWORDS = ["eq", "gt", "gte", "lt", "lte", "nil", "not_nil"].freeze
6
6
  STRING_COMPARISON_KEYWORDS = ["contains", "contains_i", "eq", "eq_i", "starts_with", "starts_with_i", "ends_with", "ends_with_i", "nil", "not_nil"].freeze
7
7
 
8
- attr_reader :apply, :arel_table, :api_doc_definition
9
-
10
- def initialize(model, raw_filter, api_doc_definition)
11
- self.query = model
12
- @arel_table = model.arel_table
13
- @raw_filter = raw_filter
14
- @api_doc_definition = api_doc_definition
8
+ attr_reader :apply, :arel_table, :api_doc_definition, :extra_filterable_attributes, :model
9
+
10
+ # Instantiates a new Filter object
11
+ #
12
+ # == Parameters:
13
+ # model::
14
+ # An AR model that acts as the base collection to be filtered
15
+ # raw_filter::
16
+ # The filter from the request query string
17
+ # api_doc_definition::
18
+ # The documented object definition from the OpenAPI doc
19
+ # extra_filterable_attributes::
20
+ # Attributes that can be used for filtering but are not documented in the OpenAPI doc. Something like `{"undocumented_column" => {"type" => "string"}}`
21
+ #
22
+ # == Returns:
23
+ # A new Filter object, call #apply to get the filtered set of results.
24
+ #
25
+ def initialize(model, raw_filter, api_doc_definition, extra_filterable_attributes = {})
26
+ self.query = model
27
+ @api_doc_definition = api_doc_definition
28
+ @arel_table = model.arel_table
29
+ @extra_filterable_attributes = extra_filterable_attributes
30
+ @model = model
31
+ @raw_filter = raw_filter
15
32
  end
16
33
 
17
34
  def apply
@@ -34,11 +51,13 @@ module Insights
34
51
  private
35
52
 
36
53
  attr_accessor :query
54
+ delegate(:arel_attribute, :to => :model)
37
55
 
38
56
  class Error < ArgumentError; end
39
57
 
40
58
  def attribute_for_key(key)
41
59
  attribute = api_doc_definition.properties[key.to_s]
60
+ attribute ||= extra_filterable_attributes[key.to_s]
42
61
  return attribute if attribute
43
62
  errors << "found unpermitted parameter: #{key}"
44
63
  nil
@@ -108,66 +127,70 @@ module Insights
108
127
  end
109
128
  end
110
129
 
130
+ def arel_lower(key)
131
+ Arel::Nodes::NamedFunction.new("LOWER", [arel_attribute(key)])
132
+ end
133
+
111
134
  def comparator_contains(key, value)
112
135
  return value.each { |v| comparator_contains(key, v) } if value.kind_of?(Array)
113
136
 
114
- self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}%", nil, true))
137
+ self.query = query.where(arel_attribute(key).matches("%#{query.sanitize_sql_like(value)}%", nil, true))
115
138
  end
116
139
 
117
140
  def comparator_contains_i(key, value)
118
141
  return value.each { |v| comparator_contains_i(key, v) } if value.kind_of?(Array)
119
142
 
120
- self.query = query.where(arel_table[key].lower.matches("%#{query.sanitize_sql_like(value.downcase)}%", nil, true))
143
+ self.query = query.where(arel_table.grouping(arel_lower(key).matches("%#{query.sanitize_sql_like(value.downcase)}%", nil, true)))
121
144
  end
122
145
 
123
146
  def comparator_starts_with(key, value)
124
- self.query = query.where(arel_table[key].matches("#{query.sanitize_sql_like(value)}%", nil, true))
147
+ self.query = query.where(arel_attribute(key).matches("#{query.sanitize_sql_like(value)}%", nil, true))
125
148
  end
126
149
 
127
150
  def comparator_starts_with_i(key, value)
128
- self.query = query.where(arel_table[key].lower.matches("#{query.sanitize_sql_like(value.downcase)}%", nil, true))
151
+ self.query = query.where(arel_table.grouping(arel_lower(key).matches("#{query.sanitize_sql_like(value.downcase)}%", nil, true)))
129
152
  end
130
153
 
131
154
  def comparator_ends_with(key, value)
132
- self.query = query.where(arel_table[key].matches("%#{query.sanitize_sql_like(value)}", nil, true))
155
+ self.query = query.where(arel_attribute(key).matches("%#{query.sanitize_sql_like(value)}", nil, true))
133
156
  end
134
157
 
135
158
  def comparator_ends_with_i(key, value)
136
- self.query = query.where(arel_table[key].lower.matches("%#{query.sanitize_sql_like(value.downcase)}", nil, true))
159
+ self.query = query.where(arel_table.grouping(arel_lower(key).matches("%#{query.sanitize_sql_like(value.downcase)}", nil, true)))
137
160
  end
138
161
 
139
162
  def comparator_eq(key, value)
140
- self.query = query.where(key => value)
163
+ self.query = query.where(arel_attribute(key).eq_any(Array(value)))
141
164
  end
142
165
 
143
166
  def comparator_eq_i(key, value)
144
167
  values = Array(value).map { |v| query.sanitize_sql_like(v.downcase) }
145
168
 
146
- self.query = query.where(arel_table[key].lower.matches_any(values))
169
+ self.query = query.where(arel_table.grouping(arel_lower(key).matches_any(values)))
147
170
  end
148
171
 
149
172
  def comparator_gt(key, value)
150
- self.query = query.where(arel_table[key].gt(value))
173
+ self.query = query.where(arel_attribute(key).gt(value))
151
174
  end
152
175
 
153
176
  def comparator_gte(key, value)
154
- self.query = query.where(arel_table[key].gteq(value))
177
+ self.query = query.where(arel_attribute(key).gteq(value))
155
178
  end
156
179
 
157
180
  def comparator_lt(key, value)
158
- self.query = query.where(arel_table[key].lt(value))
181
+ self.query = query.where(arel_attribute(key).lt(value))
159
182
  end
160
183
 
161
184
  def comparator_lte(key, value)
162
- self.query = query.where(arel_table[key].lteq(value))
185
+ self.query = query.where(arel_attribute(key).lteq(value))
163
186
  end
164
187
 
165
188
  def comparator_nil(key, _value = nil)
166
- self.query = query.where(key => nil)
189
+ self.query = query.where(arel_attribute(key).eq(nil))
167
190
  end
168
191
 
169
192
  def comparator_not_nil(key, _value = nil)
170
- self.query = query.where.not(key => nil)
193
+ self.query = query.where.not(arel_attribute(key).eq(nil))
171
194
  end
172
195
  end
173
196
  end
@@ -96,11 +96,6 @@ module Insights
96
96
  "description" => "ID of the resource",
97
97
  "pattern" => "^\\d+$",
98
98
  "readOnly" => true,
99
- },
100
- "SortByAttribute" => {
101
- "type" => "string",
102
- "description" => "Attribute with optional order to sort the result set by.",
103
- "pattern" => "^[a-z\\-_]+(:asc|:desc)?$"
104
99
  }
105
100
  }
106
101
  end
@@ -179,11 +174,10 @@ module Insights
179
174
  "name" => "sort_by",
180
175
  "description" => "The list of attribute and order to sort the result set by.",
181
176
  "required" => false,
177
+ "style" => "deepObject",
178
+ "explode" => true,
182
179
  "schema" => {
183
- "oneOf" => [
184
- { "$ref" => "##{SCHEMAS_PATH}/SortByAttribute" },
185
- { "type" => "array", "items" => { "$ref" => "##{SCHEMAS_PATH}/SortByAttribute" } }
186
- ]
180
+ "type" => "object"
187
181
  }
188
182
  }
189
183
  }
@@ -2,35 +2,125 @@ module Insights
2
2
  module API
3
3
  module Common
4
4
  class PaginatedResponseV2 < PaginatedResponse
5
+ # GraphQL name regex: /[_A-Za-z][_0-9A-Za-z]*/
6
+ ASSOCIATION_COUNT_ATTR = "__count".freeze
7
+
5
8
  attr_reader :limit, :offset, :sort_by
6
9
 
10
+ def records
11
+ @records ||= begin
12
+ res = @base_query.order(:id).limit(limit).offset(offset)
13
+
14
+ select_for_associations, group_by_associations = sort_by_associations_query_parameters
15
+ res = res.select(*select_for_associations) if select_for_associations.present?
16
+ res = res.left_outer_joins(*sort_by_associations) if sort_by_associations.present?
17
+ res = res.group(group_by_associations) if group_by_associations.present?
18
+
19
+ order_options = sort_by_options(res.klass)
20
+ res = res.reorder(order_options) if order_options.present?
21
+ res
22
+ end
23
+ end
24
+
25
+ # Condenses parameter values for handling multi-level associations
26
+ # and returns an array of key, value pairs.
27
+ #
28
+ # Examples:
29
+ #
30
+ # Input: { "association" => { "attribute" => "value" }, "direct_attribute" => "value2" }
31
+ # Output: [["association.attribute", "value"], ["direct_attribute", "value2"]]
32
+ #
33
+ # Input: { "association" => { "attribute" => "value" }, "association2" => { "attribute2" => "value2" } }
34
+ # Output: [["association.attribute", "value"], ["association2.attribute2", "value2"]]
35
+ #
36
+ # Input: { "association" => { "attribute1" => "value1", "attribute2" => "value2" } }
37
+ # Output: [["association.attribute1", "value1"], ["association.attribute2", "value2"]]
38
+ #
39
+ def compact_parameter(param)
40
+ result = []
41
+ return result if param.blank?
42
+
43
+ param.each do |k, v|
44
+ result += if v.kind_of?(Hash) || v.kind_of?(ActionController::Parameters)
45
+ Hash(v).map { |ak, av| ["#{k}.#{ak}", av] }
46
+ else
47
+ [[k, v]]
48
+ end
49
+ end
50
+ result
51
+ end
52
+
7
53
  private
8
54
 
9
55
  def sort_by_options(model)
10
56
  @sort_by_options ||= begin
11
- sort_options = []
12
- return sort_options if sort_by.blank?
13
-
14
- sort_by.each do |sort_attr, sort_order|
57
+ compact_parameter(sort_by).collect do |sort_attr, sort_order|
15
58
  sort_order = "asc" if sort_order.blank?
16
- arel = model.arel_attribute(sort_attr)
17
- arel = (sort_order == "desc") ? arel.desc : arel.asc
18
- sort_options << arel
59
+ arel = if sort_attr.include?('.')
60
+ association, sort_attr = sort_attr.split('.')
61
+ association_class = association.classify.constantize
62
+ if sort_attr == ASSOCIATION_COUNT_ATTR
63
+ Arel.sql("COUNT (#{association_class.table_name}.id)")
64
+ else
65
+ association_class.arel_attribute(sort_attr)
66
+ end
67
+ else
68
+ model.arel_attribute(sort_attr)
69
+ end
70
+ (sort_order == "desc") ? arel.desc : arel.asc
71
+ end
72
+ end
73
+ end
74
+
75
+ def sort_by_associations
76
+ @sort_by_associations ||= begin
77
+ compact_parameter(sort_by).collect do |sort_attr, sort_order|
78
+ next unless sort_attr.include?('.')
79
+
80
+ sort_attr.split('.').first.to_sym
81
+ end.compact.uniq
82
+ end
83
+ end
84
+
85
+ def sort_by_associations_query_parameters
86
+ select_for_associations = []
87
+ group_by_associations = []
88
+ count_selects = []
89
+
90
+ compact_parameter(sort_by).each do |sort_attr, _sort_order|
91
+ next unless sort_attr.include?('.')
92
+
93
+ association, attr = sort_attr.split('.')
94
+
95
+ base_id = "#{@base_query.table_name}.id"
96
+ base_all = "#{@base_query.table_name}.*"
97
+ select_for_associations << base_id << base_all if select_for_associations.empty?
98
+ group_by_associations << base_id << base_all if group_by_associations.empty?
99
+
100
+ if attr == ASSOCIATION_COUNT_ATTR
101
+ count_selects << Arel.sql("COUNT (#{association.classify.constantize.table_name}.id)")
102
+ else
103
+ arel_attr = association.classify.constantize.arel_attribute(attr)
104
+ association_attr = "#{association}_#{attr}"
105
+ select_for_associations << arel_attr.as(association_attr)
106
+ group_by_associations << association_attr
19
107
  end
20
- sort_options
21
108
  end
109
+ select_for_associations.append(*count_selects) unless count_selects.empty?
110
+
111
+ [select_for_associations.compact.uniq, group_by_associations.compact.uniq]
22
112
  end
23
113
 
24
114
  def validate_sort_by
25
115
  return unless sort_by.present?
26
116
  raise ArgumentError, "Invalid sort_by parameter specified \"#{sort_by}\"" unless sort_by.kind_of?(ActionController::Parameters) || sort_by.kind_of?(Hash)
27
117
 
28
- sort_by.each { |sort_attr, sort_order| validate_sort_by_directive(sort_attr, sort_order) }
118
+ compact_parameter(sort_by).each { |sort_attr, sort_order| validate_sort_by_directive(sort_attr, sort_order) }
29
119
  end
30
120
 
31
121
  def validate_sort_by_directive(sort_attr, sort_order)
32
122
  order = sort_order.blank? ? "asc" : sort_order
33
- raise ArgumentError, "Invalid sort_by directive specified \"#{sort_attr}=#{sort_order}\"" unless sort_attr.match?(/^[a-z\\-_]+$/) && order.match?(/^(asc|desc)$/)
123
+ raise ArgumentError, "Invalid sort_by directive specified \"#{sort_attr}=#{sort_order}\"" unless sort_attr.match?(/^[a-z\-_\.]+$/) && order.match?(/^(asc|desc)$/)
34
124
  end
35
125
  end
36
126
  end
@@ -5,32 +5,45 @@ module Insights
5
5
  class Access
6
6
  attr_reader :acl
7
7
  DEFAULT_LIMIT = 500
8
- def initialize(resource, verb)
9
- @resource = resource
10
- @verb = verb
11
- @regexp = Regexp.new(":(#{Regexp.escape(@resource)}|\\*):(#{Regexp.escape(@verb)}|\\*)")
12
- @app_name = ENV["APP_NAME"]
8
+ ADMIN_SCOPE = "admin"
9
+ GROUP_SCOPE = "group"
10
+ USER_SCOPE = "user"
11
+
12
+ def initialize(app_name_filter = ENV["APP_NAME"])
13
+ @app_name_filter = app_name_filter
13
14
  end
14
15
 
15
16
  def process
16
17
  Service.call(RBACApiClient::AccessApi) do |api|
17
- @acl ||= Service.paginate(api, :get_principal_access, {:limit => DEFAULT_LIMIT}, @app_name).select do |item|
18
- @regexp.match?(item.permission)
19
- end
18
+ @acls ||= Service.paginate(api, :get_principal_access, {:limit => DEFAULT_LIMIT}, @app_name_filter).to_a
20
19
  end
21
20
  self
22
21
  end
23
22
 
24
- def accessible?
25
- @acl.any?
23
+ def scopes(resource, verb, app_name = ENV['APP_NAME'])
24
+ regexp = create_regexp(app_name, resource, verb)
25
+ @acls.each_with_object([]) do |item, memo|
26
+ if regexp.match?(item.permission)
27
+ memo << all_scopes(item)
28
+ end
29
+ end.flatten.uniq.sort
30
+ end
31
+
32
+ def accessible?(resource, verb, app_name = ENV['APP_NAME'])
33
+ regexp = create_regexp(app_name, resource, verb)
34
+ @acls.any? { |item| regexp.match?(item.permission) }
35
+ end
36
+
37
+ def admin_scope?(resource, verb, app_name = ENV['APP_NAME'])
38
+ scope?(app_name, resource, verb, ADMIN_SCOPE)
26
39
  end
27
40
 
28
- def id_list
29
- ids.include?('*') ? [] : ids
41
+ def group_scope?(resource, verb, app_name = ENV['APP_NAME'])
42
+ scope?(app_name, resource, verb, GROUP_SCOPE)
30
43
  end
31
44
 
32
- def owner_scoped?
33
- ids.include?('*') ? false : owner_scope_filter?
45
+ def user_scope?(resource, verb, app_name = ENV['APP_NAME'])
46
+ scope?(app_name, resource, verb, USER_SCOPE)
34
47
  end
35
48
 
36
49
  def self.enabled?
@@ -39,26 +52,33 @@ module Insights
39
52
 
40
53
  private
41
54
 
42
- def ids
43
- @ids ||= @acl.each_with_object([]) do |item, ids|
44
- item.resource_definitions.each do |rd|
45
- next unless rd.attribute_filter.key == 'id'
46
- next unless rd.attribute_filter.operation == 'equal'
55
+ def scope?(app_name, resource, verb, scope)
56
+ regexp = create_regexp(app_name, resource, verb)
57
+ @acls.any? do |item|
58
+ regexp.match?(item.permission) && scope_matches?(item, scope)
59
+ end
60
+ end
47
61
 
48
- ids << rd.attribute_filter.value
49
- end
62
+ def scope_matches?(item, scope)
63
+ item.resource_definitions.any? do |rd|
64
+ rd.attribute_filter.key == 'scope' &&
65
+ rd.attribute_filter.operation == 'equal' &&
66
+ rd.attribute_filter.value == scope
50
67
  end
51
68
  end
52
69
 
53
- def owner_scope_filter?
54
- @acl.any? do |item|
55
- item.resource_definitions.any? do |rd|
56
- rd.attribute_filter.key == 'owner' &&
57
- rd.attribute_filter.operation == 'equal' &&
58
- rd.attribute_filter.value == '{{username}}'
70
+ def all_scopes(item)
71
+ item.resource_definitions.each_with_object([]) do |rd, memo|
72
+ if rd.attribute_filter.key == 'scope' &&
73
+ rd.attribute_filter.operation == 'equal'
74
+ memo << rd.attribute_filter.value
59
75
  end
60
76
  end
61
77
  end
78
+
79
+ def create_regexp(app_name, resource, verb)
80
+ Regexp.new("(#{Regexp.escape(app_name)}):(#{Regexp.escape(resource)}):(#{Regexp.escape(verb)})")
81
+ end
62
82
  end
63
83
  end
64
84
  end
@@ -8,9 +8,9 @@ module Insights
8
8
  class TimedOutError < StandardError; end
9
9
 
10
10
  class Service
11
- def self.call(klass)
11
+ def self.call(klass, extra_headers = {})
12
12
  setup
13
- yield init(klass)
13
+ yield init(klass, extra_headers)
14
14
  rescue RBACApiClient::ApiError => err
15
15
  raise TimedOutError.new('Connection timed out') if err.code.nil?
16
16
  raise NetworkError.new(err.message) if err.code.zero?
@@ -55,8 +55,8 @@ module Insights
55
55
  end
56
56
  end
57
57
 
58
- private_class_method def self.init(klass)
59
- headers = Insights::API::Common::Request.current_forwardable
58
+ private_class_method def self.init(klass, extra_headers)
59
+ headers = Insights::API::Common::Request.current_forwardable.merge(extra_headers)
60
60
  klass.new.tap do |api|
61
61
  api.api_client.default_headers = api.api_client.default_headers.merge(headers)
62
62
  end
@@ -1,7 +1,7 @@
1
1
  module Insights
2
2
  module API
3
3
  module Common
4
- VERSION = "3.7.0".freeze
4
+ VERSION = "3.8.0".freeze
5
5
  end
6
6
  end
7
7
  end
@@ -24,10 +24,19 @@ RSpec.shared_context "rbac_objects" do
24
24
  let(:resource_def3) { instance_double(RBACApiClient::ResourceDefinition, :attribute_filter => filter3) }
25
25
  let(:filter4) { instance_double(RBACApiClient::ResourceDefinitionFilter, :key => 'id', :operation => 'equal', :value => '*') }
26
26
  let(:resource_def4) { instance_double(RBACApiClient::ResourceDefinition, :attribute_filter => filter4) }
27
+ let(:filter5) { instance_double(RBACApiClient::ResourceDefinitionFilter, :key => 'scope', :operation => 'equal', :value => 'admin') }
28
+ let(:resource_def5) { instance_double(RBACApiClient::ResourceDefinition, :attribute_filter => filter5) }
29
+ let(:filter6) { instance_double(RBACApiClient::ResourceDefinitionFilter, :key => 'scope', :operation => 'equal', :value => 'group') }
30
+ let(:resource_def6) { instance_double(RBACApiClient::ResourceDefinition, :attribute_filter => filter6) }
31
+ let(:filter7) { instance_double(RBACApiClient::ResourceDefinitionFilter, :key => 'scope', :operation => 'equal', :value => 'user') }
32
+ let(:resource_def7) { instance_double(RBACApiClient::ResourceDefinition, :attribute_filter => filter7) }
27
33
  let(:access1) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:read", :resource_definitions => [resource_def1]) }
28
34
  let(:access2) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:write", :resource_definitions => [resource_def2]) }
29
35
  let(:access3) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:order", :resource_definitions => []) }
30
36
  let(:admin_access) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:read", :resource_definitions => [resource_def4]) }
37
+ let(:admin_scope) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:read", :resource_definitions => [resource_def5]) }
38
+ let(:group_scope) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:read", :resource_definitions => [resource_def6]) }
39
+ let(:user_scope) { instance_double(RBACApiClient::Access, :permission => "#{app_name}:#{resource}:read", :resource_definitions => [resource_def7]) }
31
40
  let(:group_uuids) { [group1.uuid, group2.uuid, group3.uuid] }
32
41
  let(:api_instance) { double }
33
42
  let(:rs_class) { class_double("Insights::API::Common::RBAC::Service").as_stubbed_const(:transfer_nested_constants => true) }
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: insights-api-common
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.7.0
4
+ version: 3.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Insights Authors
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-10 00:00:00.000000000 Z
11
+ date: 2020-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: acts_as_tenant