Module: Upkeep::Rails::Cable::SubscriberIdentity

Defined in:
lib/upkeep/rails/cable/subscriber_identity.rb

Constant Summary collapse

ANONYMOUS_PUBLIC_MODE =
"anonymous_public"
IDENTIFIED_MODE =
"identified"
CONNECTION_IDENTITY_SOURCES =
%w[Current.user cookie current_attribute session warden_user].freeze

Class Method Summary collapse

Class Method Details

.active_record?(value) ⇒ Boolean

Returns:

  • (Boolean)


159
160
161
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 159

def active_record?(value)
  defined?(::ActiveRecord::Base) && value.is_a?(::ActiveRecord::Base)
end

.active_record_component(name, record) ⇒ Object



163
164
165
166
167
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 163

def active_record_component(name, record)
  raise UnidentifiedSubscriber, "ActionCable identifier #{name} is an unsaved record" unless record.id

  model_component(name, model: record.class.name, id: record.id)
end

.anonymous_componentsObject



102
103
104
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 102

def anonymous_components
  [ scalar_component(:anonymous_public_subscription, SecureRandom.uuid) ]
end

.blank?(value) ⇒ Boolean

Returns:

  • (Boolean)


208
209
210
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 208

def blank?(value)
  value.nil? || value == ""
end

.component_for(name, value) ⇒ Object



145
146
147
148
149
150
151
152
153
154
155
156
157
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 145

def component_for(name, value)
  raise UnidentifiedSubscriber, "ActionCable identifier #{name} is nil" if value.nil?

  if active_record?(value)
    active_record_component(name, value)
  elsif scalar?(value)
    scalar_component(name, value)
  elsif value.respond_to?(:to_gid_param)
    global_id_component(name, value)
  else
    raise UnidentifiedSubscriber, "ActionCable identifier #{name} has no canonical identity"
  end
end

.component_for_dependency(dependency) ⇒ Object



120
121
122
123
124
125
126
127
128
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 120

def component_for_dependency(dependency)
  if dependency.source == :current_attribute && current_user_dependency?(dependency)
    model_component(:current_user, dependency.key.fetch(:value))
  elsif dependency.source == "Current.user"
    model_component(:current_user, dependency.)
  elsif dependency.source == :warden_user
    model_component(:"warden_#{dependency..fetch(:scope)}", dependency.key.fetch(:value))
  end
end

.connection_identity_dependency?(dependency) ⇒ Boolean

Returns:

  • (Boolean)


116
117
118
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 116

def connection_identity_dependency?(dependency)
  CONNECTION_IDENTITY_SOURCES.include?(dependency.source.to_s)
end

.current_user_dependency?(dependency) ⇒ Boolean

Returns:

  • (Boolean)


130
131
132
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 130

def current_user_dependency?(dependency)
  dependency..fetch(:name) == "user"
end

.decision_for(_request = nil, recorder:) ⇒ Object



55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 55

def decision_for(_request = nil, recorder:)
  dependencies = identity_dependencies(recorder)
  if dependencies.empty?
    Decision.new(ANONYMOUS_PUBLIC_MODE, true, nil, [])
  else
    Decision.new(
      IDENTIFIED_MODE,
      false,
      "identity_dependencies_present",
      dependencies.map { |dependency| dependency.source.to_s }.uniq.sort
    )
  end
end

.derive(connection) ⇒ Object



22
23
24
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 22

def derive(connection)
  derive_all(connection).last
end

.derive_all(connection) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 26

def derive_all(connection)
  identities = []
  request_components = request_components(connection.request) if connection.respond_to?(:request)
  identifier_components = identifier_components(connection)

  identities << for_components(request_components) if request_components&.any?
  identities << for_components(Array(request_components) + identifier_components) if identifier_components.any?
  identities = identities.uniq(&:subscriber_id)

  raise UnidentifiedSubscriber, "ActionCable connection has no server identifiers" if identities.empty?

  identities
end

.derive_from_request(request, recorder:, decision: decision_for(request, recorder: recorder)) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 40

def derive_from_request(request, recorder:, decision: decision_for(request, recorder: recorder))
  components = if decision.anonymous
    anonymous_components
  else
    request_components(request) + recorder_components(recorder)
  end

  if components.empty?
    raise UnidentifiedSubscriber,
      "subscription has identity dependencies but no canonical request or recorder identity"
  end

  for_components(components)
end

.for_components(components) ⇒ Object



85
86
87
88
89
90
91
92
93
94
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 85

def for_components(components)
  canonical_bytes = JSON.generate(components.sort_by { |component| component.fetch(:name) })
  subscriber_id = "action_cable:#{Digest::SHA256.hexdigest(canonical_bytes)}"

  Identity.new(
    subscriber_id,
    Delivery::ActionCableAdapter.stream_name_for(subscriber_id),
    components
  )
end

.for_identifiers(identifiers) ⇒ Object



81
82
83
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 81

def for_identifiers(identifiers)
  for_components(identifiers.map { |name, value| component_for(name, value) })
end

.global_id_component(name, value) ⇒ Object



186
187
188
189
190
191
192
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 186

def global_id_component(name, value)
  {
    name: name.to_s,
    kind: "global_id",
    value: value.to_gid_param
  }
end

.identifier_components(connection) ⇒ Object



69
70
71
72
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 69

def identifier_components(connection)
  identifiers = Array(connection.identifiers)
  identifiers.map { |name| component_for(name, connection.public_send(name)) }
end

.identity_dependencies(recorder) ⇒ Object



106
107
108
109
110
111
112
113
114
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 106

def identity_dependencies(recorder)
  return [] unless recorder

  recorder.graph.dependency_nodes
    .map(&:payload)
    .select(&:identity?)
    .select { |dependency| connection_identity_dependency?(dependency) }
    .uniq(&:cache_key)
end

.model_component(name, identity) ⇒ Object



134
135
136
137
138
139
140
141
142
143
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 134

def model_component(name, identity)
  return unless identity.is_a?(Hash) && identity[:model] && identity[:id]

  {
    name: name.to_s,
    kind: "model",
    model: identity.fetch(:model),
    id: identity.fetch(:id).to_s
  }
end

.recorder_components(recorder) ⇒ Object



96
97
98
99
100
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 96

def recorder_components(recorder)
  identity_dependencies(recorder)
    .filter_map { |dependency| component_for_dependency(dependency) }
    .uniq
end

.request_components(request) ⇒ Object



74
75
76
77
78
79
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 74

def request_components(request)
  session_id = session_id_for(request)
  return [] unless session_id

  [scalar_component(:rails_session, session_id)]
end

.scalar?(value) ⇒ Boolean

Returns:

  • (Boolean)


169
170
171
172
173
174
175
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 169

def scalar?(value)
  value.is_a?(String) ||
    value.is_a?(Symbol) ||
    value.is_a?(Integer) ||
    value == true ||
    value == false
end

.scalar_component(name, value) ⇒ Object



177
178
179
180
181
182
183
184
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 177

def scalar_component(name, value)
  {
    name: name.to_s,
    kind: "scalar",
    class: value.class.name,
    value: value.to_s
  }
end

.session_id_for(request) ⇒ Object



194
195
196
197
198
199
200
201
202
203
204
205
206
# File 'lib/upkeep/rails/cable/subscriber_identity.rb', line 194

def session_id_for(request)
  return unless request&.respond_to?(:session)

  session = request.session
  session_id = session.id if session.respond_to?(:id)
  session_id = session_id.public_id if session_id.respond_to?(:public_id)
  session_id = session_id.private_id if session_id.respond_to?(:private_id)
  session_id = session[:session_id] if blank?(session_id) && session.respond_to?(:[])

  session_id.to_s unless blank?(session_id)
rescue StandardError
  nil
end