Module: PhiAttrs::PhiRecord

Extended by:
ActiveSupport::Concern
Defined in:
lib/phi_attrs/phi_record.rb

Overview

Module for extending ActiveRecord models to handle PHI access logging and restrict access to attributes.

Author:

  • Apsis Labs

Since:

  • 0.1.0

Instance Method Summary collapse

Instance Method Details

#__phi_extended_methodsArray<String>

Get all method names to be wrapped with PHI access extension

Returns:

  • (Array<String>)

    the method names to be wrapped with PHI access extension

Since:

  • 0.1.0



330
331
332
# File 'lib/phi_attrs/phi_record.rb', line 330

def __phi_extended_methods
  self.class.__phi_extend_methods.to_a
end

#__phi_wrapped_methodsArray<String>

Get all method names to be wrapped with PHI access logging

Returns:

  • (Array<String>)

    the method names to be wrapped with PHI access logging

Since:

  • 0.1.0



319
320
321
322
323
324
# File 'lib/phi_attrs/phi_record.rb', line 319

def __phi_wrapped_methods
  excluded_methods = self.class.__phi_exclude_methods.to_a
  included_methods = self.class.__phi_include_methods.to_a

  attribute_names - excluded_methods + included_methods - [self.class.primary_key]
end

#allow_phi(user_id = nil, reason = nil) { ... } ⇒ Object

Enable PHI access for a single instance of this class inside the block. Nested calls to allow_phi will log once per nested call

Examples:

foo = Foo.find(1)
foo.allow_phi('user@example.com', 'viewing patient record') do
 # PHI Access Allowed Here
end
# PHI Access Disallowed Here

Parameters:

  • user_id (String) (defaults to: nil)

    A unique identifier for the person accessing the PHI

  • reason (String) (defaults to: nil)

    The reason for accessing PHI

Yields:

  • The block in which phi access is allowed

Since:

  • 0.1.0



379
380
381
382
# File 'lib/phi_attrs/phi_record.rb', line 379

def allow_phi(user_id = nil, reason = nil, &block)
  get_phi(user_id, reason, &block)
  nil
end

#allow_phi!(user_id = nil, reason = nil) ⇒ Object

Enable PHI access for a single instance of this class.

Examples:

foo = Foo.find(1)
foo.allow_phi!('user@example.com', 'viewing patient record')

Parameters:

  • user_id (String) (defaults to: nil)

    A unique identifier for the person accessing the PHI

  • reason (String) (defaults to: nil)

    The reason for accessing PHI

Raises:

  • (ArgumentError)

Since:

  • 0.1.0



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
# File 'lib/phi_attrs/phi_record.rb', line 343

def allow_phi!(user_id = nil, reason = nil)
  raise ArgumentError, 'block not allowed. use allow_phi with block' if block_given?

  user_id ||= self.class.current_user
  reason ||= self.class.i18n_reason
  raise ArgumentError, 'user_id and reason cannot be blank' if user_id.blank? || reason.blank?

  user_id = user_id.to_s.gsub(/[\r\n]/, " ")
  reason = reason.to_s.gsub(/[\r\n]/, " ")
  uuid = SecureRandom.uuid
  @__phi_access_stack.push(PhiStackEntry.new(
                             phi_access_allowed: true,
                             user_id: user_id,
                             reason: reason,
                             uuid: uuid,
                           ))

  PhiAttrs::Logger.tagged(*phi_log_keys, uuid) do
    PhiAttrs::Logger.info("PHI Access Enabled for '#{user_id}': #{reason}")
  end
end

#disallow_last_phi!(preserve_extensions: false) ⇒ Object

Revoke last PHI access for a single instance of this class.

Examples:

foo = Foo.find(1)
foo.disallow_last_phi!

Raises:

  • (ArgumentError)

Since:

  • 0.1.0



470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/phi_attrs/phi_record.rb', line 470

def disallow_last_phi!(preserve_extensions: false)
  raise ArgumentError, 'block not allowed' if block_given?

  removed_access = @__phi_access_stack.pop
  uuid = removed_access&.uuid

  PhiAttrs::Logger.tagged(*phi_log_keys, uuid) do
    revoke_extended_phi! unless preserve_extensions
    message = removed_access.present? ? "PHI access disabled for #{removed_access.user_id}" : 'PHI access disabled. No instance level access was granted.'
    PhiAttrs::Logger.info(message)
  end
end

#disallow_phi { ... } ⇒ Object

Dissables PHI access for a single instance of this class inside the block. Nested calls to allow_phi will log once per nested call

Examples:

foo = Foo.find(1)
foo.allow_phi('user@example.com', 'viewing patient record') do
 # PHI Access Allowed Here
end
# PHI Access Disallowed Here

Parameters:

  • user_id (String)

    A unique identifier for the person accessing the PHI

  • reason (String)

    The reason for accessing PHI

Yields:

  • The block in which phi access is allowed

Raises:

  • (ArgumentError)

Since:

  • 0.1.0



451
452
453
454
455
456
457
458
459
460
461
462
# File 'lib/phi_attrs/phi_record.rb', line 451

def disallow_phi
  raise ArgumentError, 'block required. use disallow_phi! without block' unless block_given?

  add_disallow_flag!
  add_disallow_flag_to_extended_phi!
  begin
    yield
  ensure
    remove_disallow_flag_from_extended_phi!
    remove_disallow_flag!
  end
end

#disallow_phi!Object

Revoke all PHI access for a single instance of this class.

Examples:

foo = Foo.find(1)
foo.disallow_phi!

Raises:

  • (ArgumentError)

Since:

  • 0.1.0



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/phi_attrs/phi_record.rb', line 421

def disallow_phi!
  raise ArgumentError, 'block not allowed. use disallow_phi with block' if block_given?

  removed_access_for_uuid = self.class.__uuid_string(@__phi_access_stack)

  PhiAttrs::Logger.tagged(*phi_log_keys, removed_access_for_uuid) do
    removed_access_for = self.class.__user_id_string(@__phi_access_stack)

    revoke_extended_phi!
    @__phi_access_stack = []

    message = removed_access_for.present? ? "PHI access disabled for #{removed_access_for}" : 'PHI access disabled. No instance level access was granted.'
    PhiAttrs::Logger.info(message)
  end
end

#get_phi(user_id = nil, reason = nil) { ... } ⇒ Object

Enable PHI access for a single instance of this class inside the block. Returns whatever is returned from the block. Nested calls to get_phi will log once per nested call s

Examples:

foo = Foo.find(1)
phi_data = foo.get_phi('user@example.com', 'viewing patient record') do
 foo.phi_field
end

Parameters:

  • user_id (String) (defaults to: nil)

    A unique identifier for the person accessing the PHI

  • reason (String) (defaults to: nil)

    The reason for accessing PHI

Yields:

  • The block in which phi access is allowed

Returns:

  • PHI

Raises:

  • (ArgumentError)

Since:

  • 0.1.0



400
401
402
403
404
405
406
407
408
409
410
411
412
413
# File 'lib/phi_attrs/phi_record.rb', line 400

def get_phi(user_id = nil, reason = nil)
  raise ArgumentError, 'block required' unless block_given?

  extended_instances = @__phi_relations_extended.clone
  begin
    allow_phi!(user_id, reason)

    yield
  ensure
    new_extensions = @__phi_relations_extended - extended_instances
    disallow_last_phi!(preserve_extensions: true)
    revoke_extended_phi!(new_extensions) if new_extensions.any?
  end
end

#phi_access_reasonString

The access reason for allowing access to this instance. This is what was passed in when PhiRecord#allow_phi! was called.

Returns:

  • (String)

    the reason passed in to allow_phi!

Since:

  • 0.1.0



499
500
501
502
503
# File 'lib/phi_attrs/phi_record.rb', line 499

def phi_access_reason
  return 'new record' if new_record?

  phi_context&.reason
end

#phi_allowed?Boolean

Whether PHI access is allowed for a single instance of this class

Examples:

foo = Foo.find(1)
foo.phi_allowed?

Returns:

  • (Boolean)

    whether PHI access is allowed for this instance

Since:

  • 0.1.0



513
514
515
# File 'lib/phi_attrs/phi_record.rb', line 513

def phi_allowed?
  new_record? || (!phi_context.nil? && phi_context.phi_access_allowed)
end

#phi_allowed_byString

The unique identifier for whom access has been allowed on this instance. This is what was passed in when PhiRecord#allow_phi! was called.

Returns:

  • (String)

    the user_id passed in to allow_phi!

Since:

  • 0.1.0



488
489
490
491
492
# File 'lib/phi_attrs/phi_record.rb', line 488

def phi_allowed_by
  return '__new_record__' if new_record?

  phi_context&.user_id
end

#reloadObject

Since:

  • 0.1.0



529
530
531
532
# File 'lib/phi_attrs/phi_record.rb', line 529

def reload
  @__phi_relations_extended.clear
  super
end

#require_phi!Object

Require phi access. Raises an error pre-emptively if it has not been granted.

Examples:

def use_phi(patient_record)
  patient_record.require_phi!
  # ...use PHI Freely
end

Raises:

Since:

  • 0.1.0



525
526
527
# File 'lib/phi_attrs/phi_record.rb', line 525

def require_phi!
  raise PhiAttrs::Exceptions::PhiAccessException, 'PHI Access required, please call allow_phi or allow_phi! first' unless phi_allowed?
end