Coder Social home page Coder Social logo

phi_attrs's Introduction

phi_attrs Gem Version Spec CI

HIPAA compliant PHI access logging for Ruby on Rails.

According to HIPAA Security Rule § 164.312(b), HIPAA covered entities are required to:

Implement hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information.

The phi_attrs gem is intended to assist with implementing logging to comply with the access log requirements of § 164.308(a)(1)(ii)(D):

Information system activity review (Required). Implement procedures to regularly review records of information system activity, such as audit logs, access reports, and security incident tracking reports.

To do so, phi_attrs extends ActiveRecord models by adding automated logging and explicit access control methods. The access control mechanism creates a separate phi_access_log.

Please Note: while phi_attrs helps facilitate access logging, it still requires due diligence by developers, both in ensuring that models and attributes which store PHI are flagged with phi_model and that calls to allow_phi! properly attribute both a unique identifier and an explicit reason for PHI access.

Please Note: there are other aspects of building a HIPAA secure application which are not addressed by phi_attrs, and as such use of phi_attrs on its own does not ensure HIPAA Compliance. For further reading on how to ensure your application meets the HIPAA security standards, review the HHS Security Series Technical Safeguards and Summary of the HIPAA Security Rule, in addition to consulting your compliance and legal counsel.

Stability

All versions of this project below 1.0.0 should be considered unstable beta software. Even minor-version updates may introduce breaking changes to the public API at this stage. We strongly suggest that you lock the installed version in your Gemfile to avoid unintended breaking updates.

Installation

Add this line to your application's Gemfile:

gem 'phi_attrs'

And then execute:

$ bundle

Or install it yourself as:

$ gem install phi_attrs

Initialize

Create an initializer to configure the PHI log file location. Log rotation can be configured with log_shift_age and log_shift_size (disabled by default).

Example:

config/initializers/phi_attrs.rb

PhiAttrs.configure do |conf|
  conf.log_path = Rails.root.join("log", "phi_access_#{Rails.env}.log")
  conf.log_shift_age  = 10            # how many logs to keep of `log_shift_size` or frequency to rotate ('daily', 'weekly' or 'monthly'). Disable rotation with 0 (default).
  conf.log_shift_size = 100.megabytes # size in bytes when using `log_shift_age` as a number
end

Usage

class PatientInfo < ActiveRecord::Base
  phi_model

  exclude_from_phi :last_name
  include_in_phi :birthday

  def birthday
    Time.current
  end
end

Access is granted on a instance level:

info = PatientInfo.new
info.allow_phi!("[email protected]", "Customer Service")

When using on an instance if you find it in a second place you will need to call allow_phi! again.

or a class:

PatientInfo.allow_phi!("[email protected]", "Customer Service")

As of version 0.1.5, a block syntax is available. As above, this is available on both class and instance levels.

Note the lack of a ! at the end. These methods should not be used alongside the mutating (bang) methods! We recommend using the block syntax for tighter control.

patient = PatientInfo.find(params[:id])
patient.allow_phi('[email protected]', 'Display Customer Data') do
  @data = patient.to_json
end # Access no longer allowed beyond this point

or a block on a class:

PatientInfo.allow_phi('[email protected]', 'Display Customer Data') do
  @data = PatientInfo.find(params[:id]).to_json
end # Access no longer allowed beyond this point

Controlling What Is PHI

When you include phi_model on your active record all fields except the id will be considered PHI.

To remove fields from PHI tracking use exclude_from_phi:

# created_at and updated_at will be accessible as normal
class PatientInfo < ActiveRecord::Base
  phi_model

  exclude_from_phi :created_at, :updated_at
end

To add a method as PHI use include_in_phi. Include takes precedence over exclude so a method that appears in both will be considered PHI.

# birthday and node will throw PHIExceptions if accessed without permission
class PatientInfo < ActiveRecord::Base
  phi_model

  include_in_phi :birthday, :note

  def birthday
    Time.current
  end

  attr_accessor :note
end

Example Usage

Example of exclude_from_phi and include_in_phi with inheritance.

class PatientInfo < ActiveRecord::Base
  phi_model
end

pi = PatientInfo.new(first_name: "Ash", last_name: "Ketchum")
pi.created_at
# PHIAccessException!
pi.last_name
# PHIAccessException!
pi.allow_phi "Ash", "Testing PHI Attrs" { pi.last_name }
# "Ketchum"
class PatientInfoTwo < PatientInfo
  exclude_from_phi :created_at
end

pi = PatientInfoTwo.new(first_name: "Ash", last_name: "Ketchum")
pi.created_at
# current time
pi.last_name
# PHIAccessException!
pi.allow_phi "Ash", "Testing PHI Attrs" { pi.last_name }
# "Ketchum"
class PatientInfoThree < PatientInfoTwo
  include_in_phi :created_at # Changed our mind
end

pi = PatientInfoThree.new(first_name: "Ash", last_name: "Ketchum")
pi.created_at
# PHIAccessException!
pi.last_name
# PHIAccessException!
pi.allow_phi "Ash", "Testing PHI Attrs" { pi.last_name }
# "Ketchum"

Extending PHI Access

Sometimes you'll have a single mental model that is composed of several ActiveRecord models joined by association. In this case, instead of calling allow_phi! on all joined models, we expose a shorthand of extending PHI access to related models.

class PatientInfo < ActiveRecord::Base
  phi_model
end

class Patient < ActiveRecord::Base
  has_one :patient_info

  phi_model

  extend_phi_access :patient_info
end

patient = Patient.new
patient.allow_phi!('[email protected]', 'reason')
patient.patient_info.first_name

NOTE: This is not intended to be used on all relationships! Only those where you intend to grant implicit access based on access to another model. In this use case, we assume that allowed access to Patient implies allowed access to PatientInfo, and therefore does not require an additional allow_phi! check. There are no guaranteed safeguards against circular extend_phi_access calls!

Check If PHI Access Is Allowed

To check if PHI is allowed for a particular instance of a class call phi_allowed?.

patient = Patient.new
patient.phi_allowed? # => false

patient.allow_phi('[email protected]', 'reason') do
  patient.phi_allowed? # => true
end

patient.phi_allowed? # => false

patient.allow_phi!('[email protected]', 'reason')
patient.phi_allowed? # => true

This also works if access was granted at the class level:

patient = Patient.new
patient.phi_allowed? # => false
Patient.allow_phi!('[email protected]', 'reason')
patient.phi_allowed? # => true

There is also a phi_allowed? check available to see at the class level.

Patient.phi_allowed? # => false
Patient.allow_phi!('[email protected]', 'reason')
Patient.phi_allowed? # => true

Note that any instance level access grants will not change class level access:

patient = Patient.new

patient.phi_allowed? # => false
Patient.phi_allowed? # => false

patient.allow_phi!('[email protected]', 'reason')

patient.phi_allowed? # => true
Patient.phi_allowed? # => false

Revoking PHI Access

You can remove access to PHI with disallow_phi!. Each disallow_phi! call removes all access granted by allow_phi! at that level (class or instance).

At a class level:

Patient.disallow_phi!

Or at a instance level:

patient.disallow_phi!
  • If access is granted at both class and instance level you will need to call disallow_phi! twice, once for the instance and once for the class.

There is also a block syntax of disallow_phi for temporary suppression phi access to the class or instance level

patient = PatientInfo.find(params[:id])
patient.allow_phi!('[email protected]', 'Display Patient Data')
patient.disallow_phi do
  @data = patient.to_json # PHIAccessException
end # Access is allowed again beyond this point

or a block level on a class:

PatientInfo.allow_phi!('[email protected]', 'Display Patient Data')
PatientInfo.disallow_phi do
  @data = PatientInfo.find(params[:id]).to_json # PHIAccessException
end # Access is allowed again beyond this point
  • Reminder instance level phi_allow will take precedent over a class level disallow_phi

Manual PHI Access Logging

If you aren't using phi_record you can still use phi_attrs to manually log phi access in your application. Where ever you are granting PHI access call:

user = '[email protected]'
message = 'accessed list of all patients'
PhiAttrs.log_phi_access(user, message)

Reason Translations

It can get cumbersome to pass around PHI Access reasons. PHI Attrs allows you to use your translations file to keep your code dry. If your translation file contains a reason for the combination of controller, action, and model you can skip passing reason:

module Admin
  class PatientDashboardController < ApplicationController
    def expelliarmus
      patient_info.allow_phi(current_user) do
        # reason tries to use `phi.admin.patient_dashbaord.expelliarmus.patient_info`
      end
    end

    def leviosa
      patient_info.allow_phi(current_user) do
        # reason tries to use `phi.admin.patient_dashbaord.leviosa.patient_info`
      end
    end
  end
end

The following en.yml file would work:

en:
  phi:
    admin:
      patient_dashboard:
        expelliarmus:
          patient_info: "Patient Disarmed"
        leviosa:
          patient_info: "Patient Levitated"

If you have a typo in your en.yml file or you choose not to provide a translation for your phi reasons your code will fail with an ArgumentError. To assist you in debugging PHI Attrs will print a :warn message with the expected location for the missing translation.

If you would like to change from phi to a custom location you can set the path in your initializer.

PhiAttrs.configure do |conf|
  conf.translation_prefix = 'custom_prefix'
end

Default User

Passing around the current user can clutter your code. PHI Attrs allows you to configure a controller method that will be called to get the currently logged in user:

config/initializers/phi_attrs.rb

PhiAttrs.configure do |conf|
  conf.current_user_method = :user_email
end

app/controllers/home_controller.rb

class ApplicationController < ActionController::Base
  private

  def user_email
    current_user&.email
  end
end

With the above code, any call to allow_phi (that starts in a controller derived from ApplicationController) will use the result of user_email as the user argument of allow_phi.

Note that if you have a default user, but choose not to use translations for reasons you'll have to pass nil as the user:

person_phi.allow_phi(nil, "Because I felt like looking at PHI") do
  # Allows PHI
end

Request UUID

It can be helpful to include the Rails request UUID to match up your general application logs to your PHI access logs. The following snippet will prepend your PHI access logs with the request UUID.

app/controllers/application_controller.rb

around_action :tag_phi_log_with_request_id

...

private

def tag_phi_log_with_request_id
  PhiAttrs::Logger.logger.tagged("Request ID: #{request.uuid}") do
    yield
  end
end

Best Practices

  • Mix and matching instance, class and block syntaxes for allowing/denying PHI is not recommended.
    • Sticking with one style in your application will make it easier to understand what access is granted and where.

Development

It is recommended to use the provided docker-compose environment for development to help ensure dependency consistency and code isolation from other projects you may be working on.

Begin

$ docker-compose up
$ bin/ssh_to_container

Tests

Tests are written using RSpec and are setup to use Appraisal to run tests over multiple rails versions.

$ bin/run_tests
or for individual tests:
$ bin/ssh_to_container
$ bundle exec appraisal rspec spec/path/to/spec.rb

To run just a particular rails version: $ bundle exec appraisal rails_6.1 rspec $ bundle exec appraisal rails-7.0 rspec

Console

An interactive prompt that will allow you to experiment with the gem.

$ bin/ssh_to_container
$ bin/console

Local Install

Run bin/setup to install dependencies. Then, run bundle exec appraisal rspec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install.

Versioning

To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/apsislabs/phi_attrs.

Any PRs should be accompanied with documentation in README.md, and changes documented in CHANGELOG.md.

License

The gem is available as open source under the terms of the MIT License.

Legal Disclaimer

Apsis Labs, LLP is not a law firm and does not provide legal advice. The information in this repo and software does not constitute legal advice, nor does usage of this software create an attorney-client relationship.

Apsis Labs, LLP is not a HIPAA covered entity, and usage of this software does not create a business associate relationship, nor does it enact a business associate agreement.

Full Disclaimer


Built by Apsis

apsis

phi_attrs was built by Apsis Labs. We love sharing what we build! Check out our other libraries on Github, and if you like our work you can hire us to build your vision.

phi_attrs's People

Contributors

crisfole avatar egreer avatar henrykeiter avatar ncallaway avatar njakobsen avatar wkirby avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

phi_attrs's Issues

I18n Support

We frequently find ourselves grabbing translations based on our current location:

class FoobarController < ApplicationController
  # ...
  def bazify
    # ...
    reason = t('foobar.bazify.phi_reason')
    # ...
  end
end

Or when we use slayer:

class FrooFrooCommand < Slayer::Command
  def call
    reason = I18n.t('commands.froofroo.phi_reason')
    # ...
  end
end

For at least controllers, commands, and services this should be formalized so that a null reason gets automatically filled out with a translation if present.

Allow Customizing Translation Location

Translations are currently hard coded into the translation namespace phi:

en:
  phi:
    controller:
      action:
        model: "Whatever"

We could easily allow that to be customized like the log location.

Update Internal Stack Behavior

Instead of using a raw stack with simple push and pops we should assign each entry a GUID so that we can have more controlled revoke behavior for easier mixing of block and ! syntax's.

  • allow_phi! should return a GUID
  • disallow_phi! should accept an optional GUID to remove
  • allow_phi and disallow_phi blocks should track the GUID they create and then revoke that particular access.

This will better support weird mixes like the following, with at least consistent behavior (even if we still don't recommend it):

patient_john = PatientInfo.new

guid = patient_john.allow_phi!('allow1', 'reason)      # Stack: 'allow1'

patient_john.disallow_phi do     # Stack: 'allow1', 'disallow1'
   patient_john.disallow_phi(guid)    # Stack: 'disallow1'
   guid = patient_john.allow_phi('allow2')    # Stack: 'disallow1',  'allow2'
end

patient_john.name # Stack: 'allow2'

Override Indexer

The index method is easy to override:

def [](key)
  super(key)
end

Should we consider overriding this as well?

Combine various `allow_phi[!]` methods

I don't think there's any reason to name the block-syntax method (allow_phi) differently from the standard use (allow_phi!). We can simply check for the existence of a block at the top. This would reduce the likelihood of doing the wrong thing by accidentally calling the wrong method.

For example, the following is currently perfectly good-looking code:

Foo.allow_phi!('henry', 'making a mistake') do
  puts Foo.first.phi_field
end

The issue is...

The block here is never run, because I accidentally used the bang method, which doesn't support blocks. 🤦‍♂️

require_phi! method

There are cases where it'd be nice to validate that an argument already permits phi access, for instance when a current_user is not available.

For those cases a simple require_phi! would be more ergonomic than raise ArgumentError, "phi_access required, please allow_phi first" unless x.allow_phi?

Implementation is super easy, obviously.

Log all access frames on the stack

With the addition of the access stack, more than one "user" may be responsible for a given PHI access request. We should update the automated access logging to surface all users on the current stack, rather than just the one on top.

# FooController
Foo.allow_phi('[email protected]', 'Displaying Foo') do
  foo = Foo.find(params[:id])
  MyService.show_foo(foo)
end

# MyService

def show_foo(foo)
  foo.allow_phi!('MyService', 'Show Foo')
  return foo.as_json # Should log something like "Foo access 0x00fd8a14 by [[email protected], MyService]"
end

extend_phi_access does not work with has_many relationships

Calling extend_phi_access does not adequately work for relationships that were defined with has_many instead of belongs to.

Example:

class MyAwesomeRecord < ApplicationRecord
  phi_model

  belongs_to :awesome_record_meta
  has_many :user_fields

  extend_phi_access :awesome_record_meta, :user_fields
end

...later

awesome = MyAwesomeRecord.first
awesome.allow_phi!("user", "context")

awesome.awesome_record_meta.meta_field # OK, works as expected
awesome.user_fields.first.user_field # Explodes with PHIAccessException

Fix will likely need to be in the phi_extend_access method in https://github.com/apsislabs/phi_attrs/blob/master/lib/phi_attrs/phi_record.rb#L286

Add shorthand for joined models

When dealing with a phi_model it's not that infrequent that we'll have joined phi_models. For convenience, we should have a shorthand method of extending allow_phi! to include those joins.

There should be two methods of doing this. One at the class level, which essentially acts as a permanent extension: allowing PHI access on this model will grant PHI access on these models. I'm thinking a syntax like:

class PatientInfo < ActiveRecord::Base
  phi_model
end

class Patient < ActiveRecord::Base
  has_one :patient_info

  phi_model
  extend_phi_access :patient_info
end

patient = Patient.new
patient.allow_phi!('[email protected]', 'reason')
patient.patient_info.first_name

Additionally, there should be a per-usage shorthand. Something that is passed as an argument to allow_phi!. I'm thinking something like:

class PatientInfo < ActiveRecord::Base
  phi_model
end

class Patient < ActiveRecord::Base
  has_one :patient_info

  phi_model
end

patient = Patient.new
patient.allow_phi!('[email protected]', 'reason', include: [:patient_info])
patient.patient_info.first_name

Implement disallow block [or remove]

Implement a disallow_phi block in a way that is fully functional, or remove the method. Personally I don't think a block for this is necessary; it seems to encourage scary behavior.

Hook into ActiveRecord::Persistance#reload

This may be unnecessary depending on the implementation of #2, but right now, if you do something like:

class PatientInfo < ActiveRecord::Base
  phi_model
end

class Patient < ActiveRecord::Base
  has_one :patient_info
  phi_model
end

patient = Patient.find(1)
patient.allow_phi!('test', 'test')
patient.patient_info.allow_phi!('test', 'test')

patient.patient_info.first_name #=> 'John'

patient.reload!

patient.patient_info.first_name #=> raise PHI Access Exception

We might be able to hook into reload to re-call allow_phi! for all the models that are currently allowed to access PHI.

Fix generator

The generator should create a simple spec file, and should not produce outdated method calls in the commands (fail! and pass!)

Don't require phi_access logging for `new` models

Sometimes you just want a transitory MyModel.new(params).method_call

It's super annoying to have to assign a variable just to allow phi on the params you're already touching and are unprotected anyway.

We shouldn't 'engage' PHI_Attrs until it's been saved to the database.

Extend PHI Access on `allow_phi` call instead of on extension method call

# model with associations
class Foo < ActiveRecord::Base
  phi_model
  belongs_to :bar
  has_many :baz

  extend_phi_access :bar, :baz
end

# setup associations
foo = Foo.new
bar = Bar.new
baz = Baz.new
foo.bar = bar
foo.baz << baz

# PHI access is not extended until we call the wrapped method
foo.allow_phi!('me', 'reason')
foo.association(:bar).reader.phi_allowed? # => false
foo.bar.phi_allowed? # => true
foo.association(:bar).reader.phi_allowed? # => true

# desired outcome
foo.allow_phi!('me', 'reason')
foo.association(:bar).reader.phi_allowed? # => true
foo.bar.phi_allowed? # => true
foo.association(:bar).reader.phi_allowed? # => true

We should update allow_phi! to proactively iterate over PHI extensions and call allow PHI on them.

Add shortcut for Rails controllers

In the context of a rails controller, we can provide a shorthand, so that we don't have to pass in an identifier or a reason. We can assume a current_user and an identification method (probably default to :email) and allow configuration of these.

For the reason, we can do something like:

def current_request
    "#{request.request_method} #{controller_name}::#{action_name}"
  end

This will give the reason as something like GET application_controller::index.

Devise User as phi_model

I have my User model as a phi_model and I have the following in the model.

phi_model
include_in_phi(*%i[
    uid
    name
    slug
    phone
    office_phone
    last_sign_in_ip
    unconfirmed_email
    current_sign_in_ip
  ])

In my ApplicationController I have a before_action that allows PHI info:

User.allow_phi!(current_user&.email, "Details: #{params[:controller]}, #{params[:action]}")

I am getting a PhiAttrs::Exceptions::PhiAccessException because I am trying to use the current_user&.email in the log but that is a field I need to include. Am I doing something wrong?

Rails 7 - Potential Select Attribute Behavior Change

There is a bug/unlisted deprecation in Rails 7 regarding .select and how it creates attribute reflections for fields. To fix Add a has_attribute? check to anything acting on after_initialize will need to have these for us to be able to use .select.

Example:
if has_attribute?(:foo) && foo.blank?

Allow one-off blocks

To extract PHI in a single location, once blocks are available we should capture the result of the block and return it, to allow constructs like this:

field_val = obj.allow_phi { obj.phi_field }

If we don't want to allow this sort of behavior (which I could see, since it explicitly "leaks" PHI to an uncontrolled location), we probably don't want blocks at all. Note that such "leaking" is already very easy to do, so this shouldn't be our only reason for not wanting this construct, e.g.

unprotected_data = []
obj.allow_phi!
unprotected_data << obj.phi_field
obj.disallow_phi!

Impersonation

We should enable impersonation. "A viewed this while impersonating B"

Clean up test suites

Tests are currently scattered in basically the order in which their functionality was written. They need reorganization and possibly also FactoryBot or another data helper.

Fix Logging for Multiple Access

The unit tests are wrong in spec/phi_attrs/logger_spec.rb lines 172, 179, 208, 215. as they are using allow! instead of block.

Need to fix the underlying logging and the tests,

`disallow_phi!` should clear the whole stack

Currently, the public disallow_phi! method just pops the top entry. This is pretty unintuitive IMO; I'd expect this call to absolutely, unequivocally disallow PHI access for whatever I've called it on. If we stick with a stack approach, I think this method should be made to clear the entire stack (both stacks in the event of a class-level call or maybe always), and the current behavior should be moved to an internal method.

If we move away from the access stack, this stops being a problem (though it should still probably be updated to remove access granted at both a class- and instance-level).

allow_phi! with block

def allow_phi should take a block that allows and then revokes phi access before and after the block.

Customize Logging

I could see it being really useful to allow customizing how logging happens, beyond just the file, it'd be nice to able to log it by HTTP POST or starting an ActiveJob or persisting the data to the database.

PHIPromise

This is something I want:

Sometimes I want to capture PHI at a certain point in time, but not use it until the future (capture before/after data, send it).

Being able to grab phi into a Promise like object and then record the access when (and if) it's actually accessed would be really useful.

# Data accessed, but Access not recorded:
before_promise = phi_object.capture_phi { |po| { secret: po.phi_protected_field } }

if update_protected_fields(phi_object)
  # Access Recorded (data *not* freshly recorded):
  before = before_promise.result
  # Data accesed *and* access recorded:
  after = phi_object.allow_phi { |po| { secret: po.phi_protected_field } }
else
  # Since before_promise.result was *not* called, the access was not logged
end

Add Logging Trace Tag

Either document how to add a trace id (like request id) or generate one so that we can track allow through revoke.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.