Class: Masks::Session
- Inherits:
-
ApplicationModel
- Object
- ApplicationModel
- Masks::Session
- 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).
Direct Known Subclasses
Masks::Sessions::Access, Masks::Sessions::Inline, Masks::Sessions::Request
Constant Summary collapse
- CHECK_KEY =
:masks
Class Method Summary collapse
Instance Method Summary collapse
-
#access(name) ⇒ Masks::Access
Returns an access class based on this session.
-
#actor=(actor) ⇒ Object
Sets the actor on the session.
-
#checks_for(type) ⇒ Hash
Returns a hash of checks for a given type.
-
#cleanup! ⇒ self
Cleans up all session data, akin to logout.
-
#data ⇒ Hash
A hash of persisted session data.
-
#device ⇒ DeviceDetector
Returns a detected device based on the
user_agent
. -
#error_message ⇒ String
A single error message for the session, as opposed to a list of errors.
-
#extra(key) ⇒ any
Returns a specific key from the session
extras
or nil. -
#extras(**opts) ⇒ Object
Additional short-lived data (not session or request data).
-
#find_check(key) ⇒ Masks::Check
Returns a check by the name provided.
-
#fingerprint ⇒ String
Returns a user-supplied “fingerprint” for the session.
-
#id ⇒ String
Returns an identifier for the session.
-
#ip_address ⇒ String
Returns the session’s IP address or nil in cases where no IP is present.
-
#mask ⇒ Masks::Mask
The mask the session will use.
- #mask! ⇒ self
-
#params ⇒ Hash
Incoming “request” params that could affect the session.
-
#passed? ⇒ Boolean
Whether or not to allow access to the actor identified by the session.
-
#passed_at ⇒ Datetime
Returns the time the session passed all checks, provided they have.
-
#passed_checks?(type = nil) ⇒ Boolean
Returns whether or not the session checks report a passing status.
-
#past_checks ⇒ Hash
Returns a hash of all checks that happened in the past.
-
#scoped ⇒ Masks::Scoped
Returns the “scoped” actor, which may be different from the actor itself.
-
#session_params ⇒ Hash
Normalizes
params[:session]
and returns it. -
#user_agent ⇒ String
Returns the session’s user agent.
-
#writable? ⇒ Boolean
Whether or not the session is “writable”.
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.
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.
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.
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.
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 |
#data ⇒ Hash
A hash of persisted session data.
93 94 95 |
# File 'app/models/masks/session.rb', line 93 def data raise NotImplementedError end |
#device ⇒ DeviceDetector
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.
83 84 85 |
# File 'app/models/masks/session.rb', line 83 def device @device ||= DeviceDetector.new(user_agent) end |
#error_message ⇒ String
A single error message for the session, as opposed to a list of errors
372 373 374 |
# File 'app/models/masks/session.rb', line 372 def errors..last end |
#extra(key) ⇒ any
Returns a specific key from the session extras
or nil.
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)
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.
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 |
#fingerprint ⇒ String
Returns a user-supplied “fingerprint” for the session.
Generally speaking, this is a low-trust value.
73 74 75 |
# File 'app/models/masks/session.rb', line 73 def fingerprint nil end |
#id ⇒ String
Returns an identifier for the session.
This value can be used to reference the session in backend processes.
48 49 50 |
# File 'app/models/masks/session.rb', line 48 def id data[:session_id] || "anonymous" end |
#ip_address ⇒ String
Returns the session’s IP address or nil in cases where no IP is present.
55 56 57 |
# File 'app/models/masks/session.rb', line 55 def ip_address nil end |
#mask ⇒ Masks::Mask
The mask the session will use.
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
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..each do || errors.add(:base, ) 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..each do || errors.add(:base, ) end elsif !passed_checks? errors.add(:base, :credentials) elsif !passed? errors.add(:base, :access) elsif !actor&.mask! errors.add(:base, actor.errors..first || :credentials) end credentials.each(&:backup!) commit_to_session end self end end |
#params ⇒ Hash
Incoming “request” params that could affect the session.
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.
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_at ⇒ Datetime
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.
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.
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_checks ⇒ Hash
Returns a hash of all checks that happened in the past.
These checks are stored in the session data, under CHECK_KEY
.
324 325 326 |
# File 'app/models/masks/session.rb', line 324 def past_checks @past_checks ||= data[CHECK_KEY]&.clone || {} end |
#scoped ⇒ Masks::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…
165 166 167 |
# File 'app/models/masks/session.rb', line 165 def scoped super || actor end |
#session_params ⇒ Hash
Normalizes params[:session]
and returns it.
Paramaters intended for the session should be nested under the session
key.
109 110 111 |
# File 'app/models/masks/session.rb', line 109 def session_params (params[:session] || {}).deep_symbolize_keys end |
#user_agent ⇒ String
Returns the session’s user agent.
Ideally this is always specified, but there are contexts where it cannot be supplied.
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.
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 |