Class: Otto::Security::Config::SecurityHeaders

Inherits:
Hash
  • Object
show all
Defined in:
lib/otto/security/config.rb

Overview

Hash-compatible storage that keeps the generic security-header API and the dedicated referrer_policy setting on one validated code path. Existing callers may continue to mutate Config#security_headers like a Hash. Every destructive Hash operation keeps the required Referrer-Policy entry canonical, validates any replacement before committing it, and prevents removal of the dedicated setting.

Constant Summary collapse

REFERRER_POLICY_HEADER =
'referrer-policy'
REFERRER_POLICY_REMOVAL_MESSAGE =
'referrer-policy cannot be removed; assign a valid referrer_policy token instead'

Instance Method Summary collapse

Constructor Details

#initialize(referrer_policy_validator) ⇒ SecurityHeaders

Returns a new instance of SecurityHeaders.



59
60
61
62
# File 'lib/otto/security/config.rb', line 59

def initialize(referrer_policy_validator)
  @referrer_policy_validator = referrer_policy_validator
  super()
end

Instance Method Details

#[]=(header, value) ⇒ Object Also known as: store



64
65
66
67
68
69
70
71
# File 'lib/otto/security/config.rb', line 64

def []=(header, value)
  if referrer_policy_header?(header)
    validated = @referrer_policy_validator.call(value).dup.freeze
    super(REFERRER_POLICY_HEADER, validated)
  else
    super
  end
end

#clear ⇒ Object

Clearing custom security headers must not remove Otto's required, dedicated referrer_policy setting.



104
105
106
107
108
109
# File 'lib/otto/security/config.rb', line 104

def clear
  policy = self[REFERRER_POLICY_HEADER]
  super
  self[REFERRER_POLICY_HEADER] = policy
  self
end

#compare_by_identity ⇒ Object

Identity comparison would make normal String lookups miss the canonical Referrer-Policy key and create apparent duplicates.

Raises:

  • (ArgumentError)


185
186
187
# File 'lib/otto/security/config.rb', line 185

def compare_by_identity
  raise ArgumentError, 'security_headers cannot use identity comparison'
end

#delete(header) ⇒ Object

Raises:

  • (ArgumentError)


111
112
113
114
115
# File 'lib/otto/security/config.rb', line 111

def delete(header, &)
  raise ArgumentError, REFERRER_POLICY_REMOVAL_MESSAGE if referrer_policy_header?(header)

  super
end

#delete_if(&block) ⇒ Object



117
118
119
120
121
# File 'lib/otto/security/config.rb', line 117

def delete_if(&block)
  return enum_for(__method__) unless block

  filter_entries!(remove_when: true, return_nil_when_unchanged: false, &block)
end

#keep_if(&block) ⇒ Object



129
130
131
132
133
# File 'lib/otto/security/config.rb', line 129

def keep_if(&block)
  return enum_for(__method__) unless block

  filter_entries!(remove_when: false, return_nil_when_unchanged: false, &block)
end

#merge!(*other_hashes) ⇒ Object Also known as: update

Raises:

  • (FrozenError)


74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# File 'lib/otto/security/config.rb', line 74

def merge!(*other_hashes)
  raise FrozenError, "can't modify frozen #{self.class}" if frozen?

  replacement = dup
  other_hashes.each do |other_hash|
    converted = Hash.try_convert(other_hash)
    raise TypeError, "no implicit conversion of #{other_hash.class} into Hash" unless converted

    converted.each_pair do |header, value|
      key = referrer_policy_header?(header) ? REFERRER_POLICY_HEADER : header
      value = yield(key, replacement[key], value) if block_given? && replacement.key?(key)
      replacement[key] = value
    end
  end

  super(replacement, &nil)
end

#reject!(&block) ⇒ Object



123
124
125
126
127
# File 'lib/otto/security/config.rb', line 123

def reject!(&block)
  return enum_for(__method__) unless block

  filter_entries!(remove_when: true, return_nil_when_unchanged: true, &block)
end

#replace(other_hash) ⇒ Object



93
94
95
96
97
98
99
100
# File 'lib/otto/security/config.rb', line 93

def replace(other_hash)
  replacement = self.class.new(@referrer_policy_validator)
  replacement.merge!(other_hash)
  replacement[REFERRER_POLICY_HEADER] = self[REFERRER_POLICY_HEADER] unless replacement.key?(
    REFERRER_POLICY_HEADER
  )
  super(replacement)
end

#select!(&block) ⇒ Object Also known as: filter!



135
136
137
138
139
# File 'lib/otto/security/config.rb', line 135

def select!(&block)
  return enum_for(__method__) unless block

  filter_entries!(remove_when: false, return_nil_when_unchanged: true, &block)
end

#shift ⇒ Object

Remove the first non-Referrer-Policy entry, keeping the dedicated setting even when it is the only entry left.

Raises:

  • (FrozenError)


144
145
146
147
148
149
150
151
# File 'lib/otto/security/config.rb', line 144

def shift
  raise FrozenError, "can't modify frozen #{self.class}" if frozen?

  header = each_key.find { |key| !referrer_policy_header?(key) }
  return nil unless header

  [header, delete(header)]
end

#transform_keys!(*args, &block) ⇒ Object

Hash#transform_keys! accepts an optional key-mapping Hash, a block, or both (the mapping wins for keys it contains). Referrer-Policy may change case but cannot be renamed to a different field.



164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
# File 'lib/otto/security/config.rb', line 164

def transform_keys!(*args, &block)
  if args.empty? && !block
    return enum_for(__method__, *args)
  elsif args.length > 1
    raise ArgumentError, "wrong number of arguments (given #{args.length}, expected 0..1)"
  end

  mapping = args.first
  replacement = self.class.new(@referrer_policy_validator)
  each_pair do |header, value|
    transformed = transformed_header(header, mapping, block)
    raise ArgumentError, REFERRER_POLICY_REMOVAL_MESSAGE if referrer_policy_header?(header) &&
                                                            !referrer_policy_header?(transformed)

    replacement[transformed] = value
  end
  replace(replacement)
end

#transform_values! ⇒ Object



153
154
155
156
157
158
159
# File 'lib/otto/security/config.rb', line 153

def transform_values!
  return enum_for(__method__) unless block_given?

  replacement = self.class.new(@referrer_policy_validator)
  each_pair { |header, value| replacement[header] = yield(value) }
  replace(replacement)
end