insights-api-common 3.7.0 → 3.8.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 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