Skip to content

Commit 7adec15

Browse files
authored
Merge pull request #1 from apsislabs/feature/logging-improvements
Feature/logging improvements
2 parents 35d2415 + 468cfba commit 7adec15

File tree

12 files changed

+142
-17
lines changed

12 files changed

+142
-17
lines changed

Appraisals

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
appraise 'rails-5.0' do
22
gem 'rails', '5.0.0'
3+
gem 'tzinfo-data'
34
end

Dockerfile

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
FROM ruby:2.5.1-alpine
2+
MAINTAINER wyatt@apsis.io
3+
4+
RUN apk add --no-cache --update \
5+
bash \
6+
alpine-sdk \
7+
sqlite-dev
8+
9+
ENV APP_HOME /app
10+
WORKDIR $APP_HOME
11+
12+
COPY . $APP_HOME/
13+
14+
EXPOSE 3000
15+
16+
CMD ["bash"]

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,28 @@ class PatientInfo < ActiveRecord::Base
3131
end
3232
```
3333

34+
Access is granted on a model level:
35+
```ruby
36+
info = new PatientInfo
37+
info.allow_phi!("allowed_user@example.com", "Customer Service")
38+
```
39+
40+
or a class:
41+
```ruby
42+
PatientInfo.allow_phi!("allowed_user@example.com", "Customer Service")
43+
```
44+
3445
## Development
3546

3647
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
3748

3849
To install this gem onto your local machine, run `bundle exec rake install`. 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](https://rubygems.org).
3950

51+
### Docker
52+
53+
* `docker-compose up`
54+
* `bin/ssh_to_container`
55+
4056
## Testing
4157

4258
$ bundle exec appraisal rspec spec/phi_attrs_spec.rb

bin/setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ set -euo pipefail
33
IFS=$'\n\t'
44
set -vx
55

6-
bundle install
6+
bundle check || bundle install
77
bundle exec appraisal install

bin/ssh_to_container

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#! /bin/bash
2+
3+
docker-compose run rails sh

docker-compose.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
version: '3'
2+
3+
services:
4+
rails:
5+
build: .
6+
volumes:
7+
- bundle_cache:/bundle
8+
- .:/app
9+
environment:
10+
- BUNDLE_JOBS=5
11+
- BUNDLE_PATH=/bundle
12+
- BUNDLE_BIN=/bundle/bin
13+
- GEM_HOME=/bundle
14+
command:
15+
- docker/start.sh
16+
17+
volumes:
18+
bundle_cache:

docker/start.sh

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env bash
2+
3+
echo "Beginning Setup"
4+
/app/bin/setup
5+
6+
echo "Environment Ready"
7+
tail -f /etc/hosts

gemfiles/rails_5.0.gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
source "https://rubygems.org"
44

55
gem "rails", "5.0.0"
6+
gem "tzinfo-data"
67

78
gemspec path: "../"

lib/phi_attrs.rb

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,18 @@
55
require 'phi_attrs/version'
66
require 'phi_attrs/configure'
77
require 'phi_attrs/railtie' if defined?(Rails)
8+
require 'phi_attrs/formatter'
89
require 'phi_attrs/logger'
910
require 'phi_attrs/exceptions'
1011
require 'phi_attrs/phi_record'
1112

1213
module PhiAttrs
1314
def phi_model(with: nil, except: nil)
1415
include PhiRecord
16+
logger = ActiveSupport::Logger.new(PhiAttrs.log_path)
17+
logger.formatter = Formatter.new
18+
file_logger = ActiveSupport::TaggedLogging.new(logger)
1519

16-
file_logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(PhiAttrs.log_path))
1720
PhiAttrs::Logger.logger = file_logger
1821
end
1922

@@ -31,5 +34,3 @@ def self.log_path=(value)
3134
@@log_path = value
3235
end
3336
end
34-
35-

lib/phi_attrs/formatter.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module PhiAttrs
2+
Format = "%s %5s: %s\n".freeze
3+
4+
# https://github.com/ruby/ruby/blob/trunk/lib/logger.rb#L587
5+
class Formatter < ::Logger::Formatter
6+
def call(severity, timestamp, progname, msg)
7+
Format % [format_datetime(timestamp), severity, msg2str(msg)]
8+
end
9+
end
10+
end

lib/phi_attrs/phi_record.rb

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,16 @@ def allow_phi!(user_id, reason)
2929
user_id: user_id,
3030
reason: reason
3131
}
32-
33-
PhiAttrs::Logger.info("PHI Access Enabled for #{user_id}: #{reason}")
32+
PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
33+
PhiAttrs::Logger.info("PHI Access Enabled for #{user_id}: #{reason}")
34+
end
3435
end
3536

3637
def disallow_phi!
3738
RequestStore.store[:phi_access].delete(name) if RequestStore.store[:phi_access].present?
38-
PhiAttrs::Logger.info('PHI access disabled')
39+
PhiAttrs::Logger.tagged(PHI_ACCESS_LOG_TAG, name) do
40+
PhiAttrs::Logger.info('PHI access disabled')
41+
end
3942
end
4043
end
4144

@@ -46,9 +49,6 @@ def initialize(*args)
4649
@__phi_access_allowed = false
4750
@__phi_access_logged = false
4851

49-
@__phi_log_id = persisted? ? attributes[self.class.primary_key] : object_id
50-
@__phi_log_key = "#{PHI_ACCESS_LOG_TAG} #{@__phi_log_id}"
51-
5252
# Wrap attributes with PHI Logger and Access Control
5353
__phi_wrapped_methods.each { |attr| phi_wrap_method(attr) }
5454
end
@@ -58,17 +58,17 @@ def __phi_wrapped_methods
5858
end
5959

6060
def allow_phi!(user_id, reason)
61-
PhiAttrs::Logger.tagged(@__phi_log_key) do
61+
PhiAttrs::Logger.tagged( *phi_log_keys ) do
6262
@__phi_access_allowed = true
6363
@__phi_user_id = user_id
6464
@__phi_access_reason = reason
6565

66-
PhiAttrs::Logger.info("PHI Access Enabled for #{user_id}: #{reason}")
66+
PhiAttrs::Logger.info("PHI Access Enabled for '#{user_id}': #{reason}")
6767
end
6868
end
6969

7070
def disallow_phi!
71-
PhiAttrs::Logger.tagged(@__phi_log_key) do
71+
PhiAttrs::Logger.tagged(*phi_log_keys) do
7272
@__phi_access_allowed = false
7373
@__phi_user_id = nil
7474
@__phi_access_reason = nil
@@ -91,18 +91,23 @@ def phi_access_reason
9191

9292
private
9393

94+
def phi_log_keys
95+
@__phi_log_id = persisted? ? "Key: #{attributes[self.class.primary_key]}" : "Object: #{object_id}"
96+
@__phi_log_keys = [PHI_ACCESS_LOG_TAG, self.class.name, @__phi_log_id]
97+
end
98+
9499
def phi_wrap_method(method_name)
95100
return if self.class.__phi_methods_wrapped.include? method_name
96101

97102
wrapped_method = :"__#{method_name}_phi_wrapped"
98103
unwrapped_method = :"__#{method_name}_phi_unwrapped"
99104

100105
self.class.send(:define_method, wrapped_method) do |*args, &block|
101-
PhiAttrs::Logger.tagged(@__phi_log_key) do
102-
raise PhiAttrs::Exceptions::PhiAccessException, "Attempted PHI acces for #{self.class.name} #{@__phi_user_id}" unless phi_allowed?
106+
PhiAttrs::Logger.tagged(*phi_log_keys) do
107+
raise PhiAttrs::Exceptions::PhiAccessException, "Attempted PHI access for #{self.class.name} #{@__phi_user_id}" unless phi_allowed?
103108

104109
unless @__phi_access_logged
105-
PhiAttrs::Logger.info("#{@__phi_user_id} accessing #{self.class.name} #{@__phi_user_id}.\n\t access logging triggered by method: #{method_name}")
110+
PhiAttrs::Logger.info("'#{phi_allowed_by}' accessing #{self.class.name}.\n\t access logging triggered by method: #{method_name}")
106111
@__phi_access_logged = true
107112
end
108113

@@ -111,7 +116,7 @@ def phi_wrap_method(method_name)
111116
end
112117

113118
# method_name => wrapped_method => unwrapped_method
114-
self.class.send(:alias_method, unwrapped_method, method_name)
119+
self.class.send(:alias_method, unwrapped_method, method_name)
115120
self.class.send(:alias_method, method_name, wrapped_method)
116121

117122
self.class.__phi_methods_wrapped << method_name

spec/phi_attrs_spec.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
context 'logging' do
2525
it 'should log an error when raising an exception' do
26+
patient_john # TODO Clean up: Logger.logger isn't defined unless we load something tagged with phi_attrs
2627
expect(PhiAttrs::Logger.logger).to receive(:error).with('my error message')
2728

2829
expect {
@@ -34,6 +35,52 @@
3435
expect(PhiAttrs::Logger.logger).to receive(:error)
3536
expect { patient_john.birthday }.to raise_error(PhiAttrs::Exceptions::PhiAccessException)
3637
end
38+
39+
it 'should log when granting phi to instance' do
40+
expect(PhiAttrs::Logger.logger).to receive(:info)
41+
patient_jane.allow_phi! 'test', 'unit tests'
42+
end
43+
44+
it 'should log when granting phi to class' do
45+
patient_john # TODO Clean up: Logger.logger isn't defined unless we load something tagged with phi_attrs
46+
expect(PhiAttrs::Logger.logger).to receive(:info)
47+
PatientInfo.allow_phi! 'test', 'unit tests'
48+
end
49+
50+
it 'should log when revokes phi to class' do
51+
patient_john # TODO Clean up: Logger.logger isn't defined unless we load something tagged with phi_attrs
52+
expect(PhiAttrs::Logger.logger).to receive(:info)
53+
PatientInfo.disallow_phi!
54+
end
55+
56+
it 'should log when accessing method' do
57+
PatientInfo.allow_phi! 'test', 'unit tests'
58+
expect(PhiAttrs::Logger.logger).to receive(:info)
59+
patient_jane.first_name
60+
end
61+
62+
it 'should log once when accessing multiple methods' do
63+
PatientInfo.allow_phi! 'test', 'unit tests'
64+
expect(PhiAttrs::Logger.logger).to receive(:info)
65+
patient_jane.first_name
66+
patient_jane.birthday
67+
end
68+
69+
it 'should log object_id for unpersisted' do
70+
PatientInfo.allow_phi! 'test', 'unit tests'
71+
expect(PhiAttrs::Logger.logger).to receive(:tagged).with(PhiAttrs::PHI_ACCESS_LOG_TAG, PatientInfo.name, "Object: #{patient_jane.object_id}").and_call_original
72+
expect(PhiAttrs::Logger.logger).to receive(:info)
73+
patient_jane.first_name
74+
end
75+
76+
it 'should log id for persisted' do
77+
PatientInfo.allow_phi! 'test', 'unit tests'
78+
patient_jane.save
79+
expect(patient_jane.persisted?).to be true
80+
expect(PhiAttrs::Logger.logger).to receive(:tagged).with(PhiAttrs::PHI_ACCESS_LOG_TAG, PatientInfo.name, "Key: #{patient_jane.id}").and_call_original
81+
expect(PhiAttrs::Logger.logger).to receive(:info).and_call_original
82+
patient_jane.first_name
83+
end
3784
end
3885

3986
context 'instance authorized' do

0 commit comments

Comments
 (0)