module PhiAttrs PHI_ACCESS_LOG_TAG = 'PHI Access Log'.freeze module PhiRecord extend ActiveSupport::Concern included do class_attribute :__phi_exclude_methods class_attribute :__phi_include_methods class_attribute :__phi_extended_methods class_attribute :__phi_methods_wrapped after_initialize :wrap_phi self.__phi_methods_wrapped = [] self.__phi_extended_methods = [] end class_methods do def exclude_from_phi(*methods) self.__phi_exclude_methods = methods.map(&:to_s) end def include_in_phi(*methods) self.__phi_include_methods = methods.map(&:to_s) end def extend_phi_access(*methods) self.__phi_extended_methods = methods.map(&:to_s) end def allow_phi!(user_id, reason) RequestStore.store[:phi_access] ||= {} RequestStore.store[:phi_access][name] ||= { phi_access_allowed: true, user_id: user_id, reason: reason } PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do PhiAttrs::Logger.info("PHI Access Enabled for #{user_id}: #{reason}") end end def disallow_phi! RequestStore.store[:phi_access].delete(name) if RequestStore.store[:phi_access].present? PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do PhiAttrs::Logger.info('PHI access disabled') end end end def wrap_phi # Disable PHI access by default @__phi_access_allowed = false @__phi_access_logged = false # Wrap attributes with PHI Logger and Access Control __phi_wrapped_methods.each { |attr| phi_wrap_method(attr) } end def __phi_wrapped_methods associations = self.class.reflect_on_all_associations.map(&:name).map(&:to_s) excluded_methods = self.class.__phi_exclude_methods.to_a included_methods = self.class.__phi_include_methods.to_a associations + attribute_names - excluded_methods + included_methods - [self.class.primary_key] end def allow_phi!(user_id, reason) PhiAttrs::Logger.tagged(*phi_log_keys) do @__phi_access_allowed = true @__phi_user_id = user_id @__phi_access_reason = reason PhiAttrs::Logger.info("PHI Access Enabled for '#{user_id}': #{reason}") end end def disallow_phi! PhiAttrs::Logger.tagged(*phi_log_keys) do @__phi_access_allowed = false @__phi_user_id = nil @__phi_access_reason = nil PhiAttrs::Logger.info('PHI access disabled') end end def phi_allowed? @__phi_access_allowed || RequestStore.store.dig(:phi_access, self.class.name, :phi_access_allowed) end def phi_allowed_by @__phi_user_id || RequestStore.store.dig(:phi_access, self.class.name, :user_id) end def phi_access_reason @__phi_access_reason || RequestStore.store.dig(:phi_access, self.class.name, :reason) end private def phi_log_keys @__phi_log_id = persisted? ? "Key: #{attributes[self.class.primary_key]}" : "Object: #{object_id}" @__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, @__phi_log_id] end def phi_wrap_method(method_name) return if self.class.__phi_methods_wrapped.include? method_name wrapped_method = :"__#{method_name}_phi_wrapped" unwrapped_method = :"__#{method_name}_phi_unwrapped" self.class.send(:define_method, wrapped_method) do |*args, &block| PhiAttrs::Logger.tagged(*phi_log_keys) do raise PhiAttrs::Exceptions::PhiAccessException, "Attempted PHI access for #{self.class.name} #{@__phi_user_id}" unless phi_allowed? unless @__phi_access_logged PhiAttrs::Logger.info("'#{phi_allowed_by}' accessing #{self.class.name}. Triggered by method: #{method_name}") @__phi_access_logged = true end # extend PHI access to relationships if needed if self.class.__phi_extended_methods.include? method_name # get the unwrapped relation relation = send(unwrapped_method, *args, &block) if relation.class.included_modules.include?(PhiRecord) relation.allow_phi!(phi_allowed_by, phi_access_reason) unless relation.phi_allowed? end end send(unwrapped_method, *args, &block) end end # method_name => wrapped_method => unwrapped_method self.class.send(:alias_method, unwrapped_method, method_name) self.class.send(:alias_method, method_name, wrapped_method) self.class.__phi_methods_wrapped << method_name end end end