Class: Masks::Session

Inherits:
ApplicationModel show all
Defined in:
app/models/masks/session.rb

Overview

Interface for sessions, which keep track of attempts to access resources.

The helper methods provided by the Masks module are wrappers around the Masks::Session class. Masks creates different types of sessions dependending on the context, calls their mask! method, and records the results.

This class is designed to be sub-classed. Sub-classes must provide a data, params, and matches_mask? method. The latter method is how a session is able to find a suitable mask from the configuration.

After a session’s mask! method is called, it will report an actor, whether or not the checks it ran have passed?, and any errors (just like an ActiveRecord model).

Constant Summary collapse

CHECK_KEY =
:masks

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.mask!(*args, **opts) ⇒ Object



38
39
40
# File 'app/models/masks/session.rb', line 38

def mask!(*args, **opts)
  new(*args, **opts).tap(&:mask!)
end

Instance Method Details

#access(name) ⇒ Masks::Access

Returns an access class based on this session.

Parameters:

  • name (String)

Returns:

Raises:



273
274
275
# File 'app/models/masks/session.rb', line 273

def access(name)
  Masks.access(name, self)
end

#actor=(actor) ⇒ Object

Sets the actor on the session.

If the actor is already set then an error will be added to the session, preventing it from passing.

Parameters:



136
137
138
139
140
141
142
# File 'app/models/masks/session.rb', line 136

def actor=(actor)
  if self.actor && actor != self.actor
    errors.add(:base, :multiple_actors)
  else
    super(actor)
  end
end

#checks_for(type) ⇒ Hash

Returns a hash of checks for a given type.

If any of the checks in the type exist on the session, it will be returned. Otherwise a new check is constructed and included in the set.

This is useful for introspecting the state of a session according to the rules of another type, but keep in mind that this does not allow the credentials configured on the type to run, so checks may report a passing status despite being stale.

Returns:

  • (Hash)


340
341
342
343
344
# File 'app/models/masks/session.rb', line 340

def checks_for(type)
  return false unless actor_checks

  load_checks(config.data.dig(:types, type.to_sym, :checks))
end

#cleanup!self

Cleans up all session data, akin to logout.

Returns:

  • (self)


252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'app/models/masks/session.rb', line 252

def cleanup!
  Masks.event "cleanup", session: self do
    mask.credentials.each do |cls|
      cred = cls.new(session: self)
      cred.cleanup!
    end

    if actor
      data[:masks] ||= {}
      data[:masks].delete(actor.session_key)
    end

    self
  end
end

#dataHash

A hash of persisted session data.

Returns:

  • (Hash)

Raises:

  • (NotImplementedError)


93
94
95
# File 'app/models/masks/session.rb', line 93

def data
  raise NotImplementedError
end

#deviceDeviceDetector

Returns a detected device based on the user_agent.

If the user agent isn’t present a detected device is still returned, but it’s attributes will return nil and its known? method will return false.

Returns:

  • (DeviceDetector)


83
84
85
# File 'app/models/masks/session.rb', line 83

def device
  @device ||= DeviceDetector.new(user_agent)
end

#error_messageString

A single error message for the session, as opposed to a list of errors

Returns:

  • (String)

    or nil



372
373
374
# File 'app/models/masks/session.rb', line 372

def error_message
  errors.full_messages.last
end

#extra(key) ⇒ any

Returns a specific key from the session extras or nil.

Parameters:

  • key (Symbol|String)

    the name of the key

Returns:

  • (any)


126
127
128
# File 'app/models/masks/session.rb', line 126

def extra(key)
  @extras&.fetch(key.to_s, nil)
end

#extras(**opts) ⇒ Object

Additional short-lived data (not session or request data)

Parameters:

  • opts (Hash)

    a hash of extra data to merge



116
117
118
119
120
# File 'app/models/masks/session.rb', line 116

def extras(**opts)
  @extras ||= {}
  @extras.merge!(**opts.stringify_keys) if opts.keys
  @extras
end

#find_check(key) ⇒ Masks::Check

Returns a check by the name provided.

Parameters:

  • key (Symbol|String)

Returns:



173
174
175
176
177
178
179
180
181
182
183
184
# File 'app/models/masks/session.rb', line 173

def find_check(key)
  return unless key&.present?

  id = key.to_sym

  unless checks[id]
    defaults = mask.checks[id] || {}
    checks[id] = Check.new(key: id, **defaults)
  end

  checks.fetch(id)
end

#fingerprintString

Returns a user-supplied “fingerprint” for the session.

Generally speaking, this is a low-trust value.

Returns:

  • (String)

    or nil



73
74
75
# File 'app/models/masks/session.rb', line 73

def fingerprint
  nil
end

#idString

Returns an identifier for the session.

This value can be used to reference the session in backend processes.

Returns:

  • (String)


48
49
50
# File 'app/models/masks/session.rb', line 48

def id
  data[:session_id] || "anonymous"
end

#ip_addressString

Returns the session’s IP address or nil in cases where no IP is present.

Returns:

  • (String)

    or nil



55
56
57
# File 'app/models/masks/session.rb', line 55

def ip_address
  nil
end

#maskMasks::Mask

The mask the session will use.

Returns:

Raises:



380
381
382
383
384
385
386
387
# File 'app/models/masks/session.rb', line 380

def mask
  @mask ||=
    begin
      mask = config.masks.find { |m| matches_mask?(m) }
      raise Error::UnknownMask, self unless mask
      mask
    end
end

#mask!self

Returns:

  • (self)


189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'app/models/masks/session.rb', line 189

def mask!
  Masks.event "session", session: self do
    return if mask&.skip?

    self.credentials =
      mask.credentials.map do |cls|
        cred = cls.new(session: self)

        # give credentials a chance to populate the session...
        # typically this is by providing an actor to validate
        if (actor = cred.lookup)
          self.actor = actor
        end

        cred
      end

    self.checks = load_checks(mask.checks)

    # session data can reveal checks that are still valid
    # based on duration. skip checking again in this case.
    return if passed?

    transaction do
      # each credential is given a chance to mask the session
      credentials.each do |cred|
        cred.mask!

        if cred.errors.any?
          cred.errors.full_messages.each do |message|
            errors.add(:base, message)
          end
        end
      rescue RuntimeError
        return cleanup!
      end

      actor&.session = self

      # to ensure that we are going to be passed?
      if actor && !actor.valid?(:mask)
        actor.errors.full_messages.each do |message|
          errors.add(:base, message)
        end
      elsif !passed_checks?
        errors.add(:base, :credentials)
      elsif !passed?
        errors.add(:base, :access)
      elsif !actor&.mask!
        errors.add(:base, actor.errors.full_messages.first || :credentials)
      end

      credentials.each(&:backup!)
      commit_to_session
    end

    self
  end
end

#paramsHash

Incoming “request” params that could affect the session.

Returns:

  • (Hash)

Raises:

  • (NotImplementedError)


100
101
102
# File 'app/models/masks/session.rb', line 100

def params
  raise NotImplementedError
end

#passed?Boolean

Whether or not to allow access to the actor identified by the session.

This method aims to be a simple test to determine whether or not a session has passed all checks.

Returns:

  • (Boolean)


296
297
298
299
300
301
302
303
304
305
# File 'app/models/masks/session.rb', line 296

def passed?
  return true if mask&.skip?
  return false if !passed_checks? || errors.any?
  return false unless matches_mask?(mask)
  return false unless actor
  return false unless mask.matches_session?(self)
  return false unless pass?

  true
end

#passed_atDatetime

Returns the time the session passed all checks, provided they have.

This method may return a time in the past—for example when credential checks passed in that past, but within their configured lifetime.

Returns:

  • (Datetime)


313
314
315
316
317
# File 'app/models/masks/session.rb', line 313

def passed_at
  return unless passed?

  checks.values.map(&:passed_at).compact.max
end

#passed_checks?(type = nil) ⇒ Boolean

Returns whether or not the session checks report a passing status.

Pass an optional type to see if the session’s checks pass according to the type’s checks, useful for determining the potential state of a session.

Parameters:

  • type (String) (defaults to: nil)

Returns:

  • (Boolean)


354
355
356
357
358
359
360
361
362
363
364
365
366
367
# File 'app/models/masks/session.rb', line 354

def passed_checks?(type = nil)
  return true unless checks.any?
  return false unless actor_checks

  to_check =
    (
      if type
        load_checks(config.data.dig(:types, type.to_sym, :checks))
      else
        checks.slice(*mask.checks.keys)
      end
    )
  to_check.values.all?(&:passed?)
end

#past_checksHash

Returns a hash of all checks that happened in the past.

These checks are stored in the session data, under CHECK_KEY.

Returns:

  • (Hash)


324
325
326
# File 'app/models/masks/session.rb', line 324

def past_checks
  @past_checks ||= data[CHECK_KEY]&.clone || {}
end

#scopedMasks::Scoped

Returns the “scoped” actor, which may be different from the actor itself.

For example, in some cases access is gained via an API key or some person/system with “admin” rights. In both cases there is an agent agent operating the system that is granted the ability to behave as someone else.

This scoped actor may respond to the methods available for interrogating scopes and roles differently than the actor itself—e.g. an access key may return a smaller set of scopes than the actor. An admin may temporarily allow additional scopes…

Returns:



165
166
167
# File 'app/models/masks/session.rb', line 165

def scoped
  super || actor
end

#session_paramsHash

Normalizes params[:session] and returns it.

Paramaters intended for the session should be nested under the session key.

Returns:

  • (Hash)


109
110
111
# File 'app/models/masks/session.rb', line 109

def session_params
  (params[:session] || {}).deep_symbolize_keys
end

#user_agentString

Returns the session’s user agent.

Ideally this is always specified, but there are contexts where it cannot be supplied.

Returns:

  • (String)

    or nil



64
65
66
# File 'app/models/masks/session.rb', line 64

def user_agent
  nil
end

#writable?Boolean

Whether or not the session is “writable”.

Some credentials only allow certain operations when in this state, which is akin to the difference between GET and POST.

Returns:

  • (Boolean)


284
285
286
287
288
# File 'app/models/masks/session.rb', line 284

def writable?
  # certain operations should only happen when the credential is in writable
  # mode, e.g. GET vs POST requests. override this method to customize the behaviour
  true
end