Class: Magick::ExportImport

Inherits:
Object
  • Object
show all
Defined in:
lib/magick/export_import.rb

Defined Under Namespace

Classes: ImportError

Constant Summary collapse

DEFAULT_MAX_IMPORT_FEATURES =

Hard cap on the number of features accepted by a single import call. Guards against DoS via an oversized payload and (combined with Admin UI auth) stops an attacker from using import as a flag-replacement primitive. Override with the MAGICK_MAX_IMPORT_FEATURES env var.

10_000

Class Method Summary collapse

Class Method Details

.apply_custom_attributes(feature, values) ⇒ Object

rubocop:enable Metrics/MethodLength rubocop:enable Metrics/CyclomaticComplexity



146
147
148
149
150
151
152
153
154
155
# File 'lib/magick/export_import.rb', line 146

def self.apply_custom_attributes(feature, values)
  return unless values.is_a?(Hash)

  values.each do |attr, rule|
    rule_h = rule.is_a?(Hash) ? rule.transform_keys(&:to_sym) : {}
    next unless rule_h[:values]

    feature.enable_for_custom_attribute(attr, rule_h[:values], operator: (rule_h[:operator] || :equals).to_sym)
  end
end

.apply_dependencies(feature, deps) ⇒ Object



168
169
170
171
172
173
# File 'lib/magick/export_import.rb', line 168

def self.apply_dependencies(feature, deps)
  list = Array(deps).map(&:to_s)
  return if list.empty?

  feature.instance_variable_set(:@dependencies, list)
end

.apply_targeting(feature, targeting) ⇒ Object

rubocop:disable Metrics/MethodLength rubocop:disable Metrics/CyclomaticComplexity



108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/magick/export_import.rb', line 108

def self.apply_targeting(feature, targeting)
  targeting.each do |type, values|
    case type.to_sym
    when :user, :users
      Array(values).each { |v| feature.enable_for_user(v) }
    when :excluded_users
      Array(values).each { |v| feature.exclude_user(v) }
    when :group, :groups
      Array(values).each { |v| feature.enable_for_group(v) }
    when :excluded_groups
      Array(values).each { |v| feature.exclude_group(v) }
    when :role, :roles
      Array(values).each { |v| feature.enable_for_role(v) }
    when :excluded_roles
      Array(values).each { |v| feature.exclude_role(v) }
    when :tag, :tags
      Array(values).each { |v| feature.enable_for_tag(v) }
    when :excluded_tags
      Array(values).each { |v| feature.exclude_tag(v) }
    when :ip_address, :ip_addresses
      feature.enable_for_ip_addresses(Array(values))
    when :excluded_ip_addresses
      feature.exclude_ip_addresses(Array(values))
    when :percentage_users
      feature.enable_percentage_of_users(values)
    when :percentage_requests
      feature.enable_percentage_of_requests(values)
    when :date_range
      range = values.is_a?(Hash) ? values.transform_keys(&:to_sym) : values
      feature.enable_for_date_range(range[:start], range[:end]) if range.is_a?(Hash) && range[:start] && range[:end]
    when :custom_attributes
      apply_custom_attributes(feature, values)
    end
  end
end

.apply_value(feature, feature_data) ⇒ Object



101
102
103
104
# File 'lib/magick/export_import.rb', line 101

def self.apply_value(feature, feature_data)
  value = fetch(feature_data, :value)
  feature.set_value(value) if !value.nil? && !(value.is_a?(String) && value.empty?)
end

.apply_variants(feature, variants) ⇒ Object



157
158
159
160
161
162
163
164
165
166
# File 'lib/magick/export_import.rb', line 157

def self.apply_variants(feature, variants)
  return unless feature.respond_to?(:add_variant)

  Array(variants).each do |v|
    h = v.is_a?(Hash) ? v.transform_keys(&:to_sym) : {}
    next unless h[:name]

    feature.add_variant(h[:name], weight: h[:weight], value: h[:value])
  end
end

.build_feature(name, feature_data, adapter_registry) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/magick/export_import.rb', line 88

def self.build_feature(name, feature_data, adapter_registry)
  Feature.new(
    name,
    adapter_registry,
    type: (fetch(feature_data, :type) || :boolean).to_sym,
    status: (fetch(feature_data, :status) || :active).to_sym,
    default_value: fetch(feature_data, :default_value),
    description: fetch(feature_data, :description),
    display_name: fetch(feature_data, :display_name),
    group: fetch(feature_data, :group)
  )
end

.export(features_hash) ⇒ Object



19
20
21
22
23
24
25
26
27
28
29
# File 'lib/magick/export_import.rb', line 19

def self.export(features_hash)
  result = features_hash.map do |_name, feature|
    feature.to_h
  end

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.exported(format: :hash, feature_count: result.length)
  end

  result
end

.export_json(features_hash) ⇒ Object



31
32
33
34
35
36
37
38
39
# File 'lib/magick/export_import.rb', line 31

def self.export_json(features_hash)
  result = JSON.pretty_generate(export(features_hash))

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.exported(format: :json, feature_count: features_hash.length)
  end

  result
end

.fetch(hash, key) ⇒ Object



77
78
79
80
81
82
83
84
85
86
# File 'lib/magick/export_import.rb', line 77

def self.fetch(hash, key)
  # Must not use `||` — falsy legitimate values (false, 0, "") would
  # silently fall through to the string-key lookup (and then to nil).
  return hash[key] if hash.key?(key)

  string_key = key.to_s
  return hash[string_key] if hash.key?(string_key)

  nil
end

.import(data, adapter_registry) ⇒ Object



41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/magick/export_import.rb', line 41

def self.import(data, adapter_registry)
  features = {}
  data = JSON.parse(data) if data.is_a?(String)
  list = Array(data)

  cap = max_import_features
  if list.size > cap
    raise ImportError,
          "Magick.import: refused to import #{list.size} features; limit is #{cap}. " \
          'Set MAGICK_MAX_IMPORT_FEATURES to override.'
  end

  list.each do |feature_data|
    unless feature_data.is_a?(Hash)
      raise ImportError, "Magick.import: each feature payload must be a Hash, got #{feature_data.class}"
    end

    name = fetch(feature_data, :name)
    next unless name

    feature = build_feature(name, feature_data, adapter_registry)
    apply_value(feature, feature_data)
    apply_targeting(feature, fetch(feature_data, :targeting) || {})
    apply_variants(feature, fetch(feature_data, :variants) || [])
    apply_dependencies(feature, fetch(feature_data, :dependencies) || [])

    features[name.to_s] = feature
  end

  if defined?(Magick::Rails::Events) && Magick::Rails::Events.rails8?
    Magick::Rails::Events.imported(format: :json, feature_count: features.length)
  end

  features
end

.max_import_featuresObject



15
16
17
# File 'lib/magick/export_import.rb', line 15

def self.max_import_features
  ENV.fetch('MAGICK_MAX_IMPORT_FEATURES', DEFAULT_MAX_IMPORT_FEATURES).to_i
end