Module: CommandTower::Audit::Payload

Defined in:
lib/command_tower/audit/payload.rb

Constant Summary collapse

SCALAR_TYPES =
[NilClass, TrueClass, FalseClass, Integer, Float, String, Symbol].freeze
MAX_DEPTH =
4
MAX_BYTES =
16_384
CHANGE_KEYS =
%w[from to].freeze

Class Method Summary collapse

Class Method Details

.enforce_size!(value) ⇒ Object



94
95
96
97
98
99
# File 'lib/command_tower/audit/payload.rb', line 94

def enforce_size!(value)
  encoded = JSON.generate(jsonify(value))
  return if encoded.bytesize <= MAX_BYTES

  raise InvalidPayloadError, "audit payload exceeds #{MAX_BYTES} bytes"
end

.jsonify(value) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/command_tower/audit/payload.rb', line 114

def jsonify(value)
  case value
  when Hash
    value.each_with_object({}) { |(key, nested), memo| memo[key.to_s] = jsonify(nested) }
  when Array
    value.map { |item| jsonify(item) }
  when Symbol
    value.to_s
  else
    value
  end
end

.stringify_keys(value) ⇒ Object



101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/command_tower/audit/payload.rb', line 101

def stringify_keys(value)
  case value
  when Hash
    value.each_with_object({}) do |(key, nested), memo|
      memo[key.to_s] = stringify_keys(nested)
    end
  when Array
    value.map { |item| stringify_keys(item) }
  else
    value
  end
end

.validate!(value, depth: 0, path: "payload") ⇒ Object



15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/command_tower/audit/payload.rb', line 15

def validate!(value, depth: 0, path: "payload")
  if depth > MAX_DEPTH
    raise InvalidPayloadError, "#{path} exceeds max nesting depth #{MAX_DEPTH}"
  end

  case value
  when Hash
    value.each do |key, nested|
      unless key.is_a?(String) || key.is_a?(Symbol)
        raise InvalidPayloadError, "#{path} has a non-string key #{key.inspect}"
      end

      validate!(nested, depth: depth + 1, path: "#{path}.#{key}")
    end
  when Array
    value.each_with_index do |item, index|
      validate!(item, depth: depth + 1, path: "#{path}[#{index}]")
    end
  else
    unless SCALAR_TYPES.any? { |type| value.is_a?(type) }
      raise InvalidPayloadError, "#{path} contains unsafe type #{value.class}"
    end
  end

  value
end

.validate_changes!(changes, allowed_keys:) ⇒ Object



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
76
77
78
79
80
81
# File 'lib/command_tower/audit/payload.rb', line 42

def validate_changes!(changes, allowed_keys:)
  unless changes.nil? || changes.is_a?(Hash)
    raise InvalidPayloadError, "changes must be a Hash"
  end

  normalized = {}
  (changes || {}).each do |key, value|
    unless key.is_a?(String) || key.is_a?(Symbol)
      raise InvalidPayloadError, "changes has a non-string key #{key.inspect}"
    end

    token = key.to_s
    unless token.match?(CommandTower::Events::SEGMENT)
      raise InvalidPayloadError, "changes key #{key.inspect} is invalid"
    end

    unless allowed_keys.map(&:to_s).include?(token)
      raise ForbiddenChangeKeyError,
        "changes key #{token} is not allowed (allowed: #{allowed_keys.join(", ").presence || "none"})"
    end

    unless value.is_a?(Hash)
      raise InvalidPayloadError, "changes.#{token} must be a Hash with from/to"
    end

    extra = value.keys.map(&:to_s) - CHANGE_KEYS
    if extra.any? || (value.keys.map(&:to_s) & CHANGE_KEYS).empty?
      raise InvalidPayloadError, "changes.#{token} must use from/to keys only"
    end

    from_value = value.key?(:from) ? value[:from] : value["from"]
    to_value = value.key?(:to) ? value[:to] : value["to"]
    validate!(from_value, depth: 2, path: "changes.#{token}.from")
    validate!(to_value, depth: 2, path: "changes.#{token}.to")
    normalized[token] = { "from" => from_value, "to" => to_value }
  end

  enforce_size!(normalized)
  normalized
end

.validate_metadata!(metadata) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'lib/command_tower/audit/payload.rb', line 83

def validate_metadata!()
  unless .nil? || .is_a?(Hash)
    raise InvalidPayloadError, "metadata must be a Hash"
  end

  validated =  || {}
  validate!(validated, path: "metadata")
  enforce_size!(validated)
  stringify_keys(validated)
end