hipaapotamus 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +20 -2
- data/README.md +60 -2
- data/Rakefile +14 -2
- data/hipaapotamus.gemspec +17 -10
- data/lib/generators/hipaapotamus/install_generator.rb +18 -0
- data/lib/generators/hipaapotamus/templates/create_hipaapotamus_actions.rb +19 -0
- data/lib/hipaapotamus.rb +59 -2
- data/lib/hipaapotamus/accountability_context.rb +97 -0
- data/lib/hipaapotamus/accountability_error.rb +2 -0
- data/lib/hipaapotamus/accountable_controller.rb +28 -0
- data/lib/hipaapotamus/action.rb +123 -0
- data/lib/hipaapotamus/agent.rb +16 -0
- data/lib/hipaapotamus/anonymous_agent.rb +6 -0
- data/lib/hipaapotamus/execution.rb +25 -0
- data/lib/hipaapotamus/policy.rb +41 -0
- data/lib/hipaapotamus/protected.rb +97 -0
- data/lib/hipaapotamus/record_callback_proxy.rb +27 -0
- data/lib/hipaapotamus/system_agent.rb +13 -0
- data/lib/hipaapotamus/version.rb +1 -1
- metadata +92 -12
- data/bin/console +0 -14
- data/bin/setup +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27838a5cec369c4c46d64d4f8e9ac240d466302c
|
4
|
+
data.tar.gz: 5b04acd2ac7021c43bd3dc11aee49fe9db48ea24
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28ae854376df17925f8caf2248aac680e2789680671c2eb7a9a35288a68427d4f27c0c587dad68f2447f7964ffac3c02383c9f5c84b5ff067a1d2da70165931e
|
7
|
+
data.tar.gz: a78f8f5289e87d662797e318a52e42d9d4c2db074231c1edac8884ed9987554f38ce575aa0aeb034a81ea9e2f3b28a521ab987affd1ff9b28c3cbe234c481180
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -1,4 +1,22 @@
|
|
1
1
|
language: ruby
|
2
|
+
|
2
3
|
rvm:
|
3
|
-
- 2.
|
4
|
-
|
4
|
+
- 2.0.0
|
5
|
+
- 2.1.7
|
6
|
+
- 2.2.3
|
7
|
+
|
8
|
+
cache: bundler
|
9
|
+
|
10
|
+
before_install:
|
11
|
+
- sudo apt-get autoremove sqlite3
|
12
|
+
- sudo apt-get install python-software-properties
|
13
|
+
- sudo apt-add-repository -y ppa:travis-ci/sqlite3
|
14
|
+
- sudo apt-get -y update
|
15
|
+
- sudo apt-cache show sqlite3
|
16
|
+
- sudo apt-get install sqlite3=3.7.15.1-1~travis1
|
17
|
+
- sudo sqlite3 -version
|
18
|
+
- gem install bundler -v 1.10.5
|
19
|
+
|
20
|
+
|
21
|
+
script:
|
22
|
+
- bundle exec rake test
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
# Hipaapotamus
|
1
|
+
# ![Hipaa Hippo](http://imgh.us/hipaa-hippo.svg)Hipaapotamus
|
2
|
+
[![Build Status](https://travis-ci.org/anarchocurious/hipaapotamus.svg)](https://travis-ci.org/anarchocurious/hipaapotamus)
|
2
3
|
|
3
4
|
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hipaapotamus`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
5
|
|
@@ -20,9 +21,65 @@ Or install it yourself as:
|
|
20
21
|
|
21
22
|
$ gem install hipaapotamus
|
22
23
|
|
24
|
+
Once the gem is installed, run:
|
25
|
+
|
26
|
+
$ rails generate hipaapotamus:install
|
27
|
+
|
28
|
+
and run:
|
29
|
+
|
30
|
+
$ rake db:migrate
|
31
|
+
|
32
|
+
to create the hipaapotamus_actions table.
|
33
|
+
|
23
34
|
## Usage
|
24
35
|
|
25
|
-
|
36
|
+
### Setting up Agents
|
37
|
+
|
38
|
+
Include Hipaapotamus::Agent on any models you want to act as an agent (for example, User) and override the hipaapotamus_display_name method to display whatever agent identifier you like:
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
class User < ActiveRecord::Base
|
42
|
+
include Hipaapotamus::Agent
|
43
|
+
|
44
|
+
def hipaapotamus_display_name
|
45
|
+
email
|
46
|
+
end
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
### Setting up Protected Models
|
51
|
+
|
52
|
+
Include Hipaapotamus::Protected on any models you want to be protected by a Hipaapotamus Policy:
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
class MedicalSecret < ActiveRecord::Base
|
56
|
+
include Hipaapotamus::Protected
|
57
|
+
end
|
58
|
+
```
|
59
|
+
|
60
|
+
### Setting up a Policy
|
61
|
+
|
62
|
+
Create a policies folder in your app directory and add your policy as follows (using MedicalSecret from above). Hipaapotamus will automatically against the policy when actions are attempted. When authorizing, the policy has access to the agent and the protected model.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
class MedicalSecretPolicy < Hipaapotamus::Policy
|
66
|
+
def access?
|
67
|
+
agent.medical_secrets.include? protected
|
68
|
+
end
|
69
|
+
|
70
|
+
def creation?
|
71
|
+
agent.medical_secrets.include? protected
|
72
|
+
end
|
73
|
+
|
74
|
+
def modification?
|
75
|
+
agent.medical_secrets.include? protected
|
76
|
+
end
|
77
|
+
|
78
|
+
def destruction?
|
79
|
+
agent.medical_secrets.include? protected
|
80
|
+
end
|
81
|
+
end
|
82
|
+
```
|
26
83
|
|
27
84
|
## Development
|
28
85
|
|
@@ -39,3 +96,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
|
|
39
96
|
|
40
97
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
41
98
|
|
99
|
+
|
data/Rakefile
CHANGED
@@ -1,6 +1,18 @@
|
|
1
|
+
require "bundler/setup"
|
2
|
+
require "hipaapotamus"
|
1
3
|
require "bundler/gem_tasks"
|
2
4
|
require "rspec/core/rake_task"
|
3
5
|
|
4
|
-
RSpec::Core::RakeTask.new(:
|
6
|
+
RSpec::Core::RakeTask.new(:test)
|
7
|
+
task spec: :test
|
5
8
|
|
6
|
-
task :
|
9
|
+
task :console do
|
10
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
11
|
+
ActiveRecord::Base.connection.execute 'CREATE TABLE hipaapotamus_actions (id integer PRIMARY KEY NOT NULL, agent_id integer, agent_type character varying NOT NULL, protected_id integer, protected_type character varying NOT NULL, serialized_protected_attributes text NOT NULL, action_type integer NOT NULL, performed_at timestamp without time zone NOT NULL, created_at timestamp without time zone NOT NULL);'
|
12
|
+
|
13
|
+
require 'pry'
|
14
|
+
Hipaapotamus.pry
|
15
|
+
end
|
16
|
+
|
17
|
+
# For travis
|
18
|
+
task default: :test
|
data/hipaapotamus.gemspec
CHANGED
@@ -4,21 +4,28 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
|
4
4
|
require 'hipaapotamus/version'
|
5
5
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name =
|
7
|
+
spec.name = 'hipaapotamus'
|
8
8
|
spec.version = Hipaapotamus::VERSION
|
9
|
-
spec.authors = [
|
10
|
-
spec.email = ["aleclarsen42@gmail.com"]
|
9
|
+
spec.authors = ['Alec Larsen', 'Jacob Lee']
|
11
10
|
|
12
11
|
spec.summary = %q{Hipaapotamus is an amazing gem for amazing people}
|
13
|
-
spec.homepage =
|
14
|
-
spec.license =
|
12
|
+
spec.homepage = 'https://github.com/anarchocurious/hipaapotamus'
|
13
|
+
spec.license = 'MIT'
|
15
14
|
|
16
15
|
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
-
spec.bindir =
|
16
|
+
spec.bindir = 'bin'
|
18
17
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
-
spec.require_paths = [
|
18
|
+
spec.require_paths = ['lib']
|
20
19
|
|
21
|
-
spec.
|
22
|
-
|
23
|
-
spec.
|
20
|
+
spec.required_ruby_version = '~> 2.0'
|
21
|
+
|
22
|
+
spec.add_runtime_dependency 'activerecord', '~> 4.1'
|
23
|
+
spec.add_runtime_dependency 'activesupport', '~> 4.1'
|
24
|
+
|
25
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
26
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.3'
|
28
|
+
spec.add_development_dependency 'pry'
|
29
|
+
spec.add_development_dependency 'sqlite3', '1.3.11'
|
30
|
+
spec.add_development_dependency 'database_cleaner', '1.0.1'
|
24
31
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
require 'rails/generators/active_record'
|
3
|
+
|
4
|
+
module Hipaapotamus
|
5
|
+
class InstallGenerator < ::Rails::Generators::Base
|
6
|
+
include ::Rails::Generators::Migration
|
7
|
+
|
8
|
+
def self.next_migration_number(dirname)
|
9
|
+
::ActiveRecord::Generators::Base.next_migration_number(dirname)
|
10
|
+
end
|
11
|
+
|
12
|
+
source_root File.expand_path('../templates', __FILE__)
|
13
|
+
|
14
|
+
def create_migration_file
|
15
|
+
migration_template 'create_hipaapotamus_actions.rb', 'db/migrate/create_hipaapotamus_actions.rb'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
class CreateHipaapotamusActions < ActiveRecord::Migration
|
2
|
+
def change
|
3
|
+
create_table :hipaapotamus_actions do |t|
|
4
|
+
t.integer :agent_id
|
5
|
+
t.string :agent_type, null: false
|
6
|
+
|
7
|
+
t.integer :protected_id
|
8
|
+
t.string :protected_type, null: false
|
9
|
+
t.text :serialized_protected_attributes, null: false
|
10
|
+
|
11
|
+
t.integer :action_type, null: false
|
12
|
+
|
13
|
+
t.boolean :is_transactional, null: false
|
14
|
+
|
15
|
+
t.datetime :performed_at, null: false
|
16
|
+
t.datetime :created_at, null: false
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/hipaapotamus.rb
CHANGED
@@ -1,5 +1,62 @@
|
|
1
|
-
require
|
1
|
+
require 'active_record'
|
2
|
+
require 'hipaapotamus/accountability_context'
|
3
|
+
require 'hipaapotamus/accountability_error'
|
4
|
+
require 'hipaapotamus/accountable_controller'
|
5
|
+
require 'hipaapotamus/action'
|
6
|
+
require 'hipaapotamus/agent'
|
7
|
+
require 'hipaapotamus/anonymous_agent'
|
8
|
+
require 'hipaapotamus/execution'
|
9
|
+
require 'hipaapotamus/policy'
|
10
|
+
require 'hipaapotamus/protected'
|
11
|
+
require 'hipaapotamus/record_callback_proxy'
|
12
|
+
require 'hipaapotamus/system_agent'
|
13
|
+
require 'hipaapotamus/version'
|
2
14
|
|
3
15
|
module Hipaapotamus
|
4
|
-
|
16
|
+
class << self
|
17
|
+
def transaction_manager
|
18
|
+
ActiveRecord::Base.connection.transaction_manager
|
19
|
+
end
|
20
|
+
|
21
|
+
def root_transaction
|
22
|
+
transaction_manager.instance_exec { @stack.try(:first) } || current_transaction
|
23
|
+
end
|
24
|
+
|
25
|
+
def current_transaction
|
26
|
+
transaction_manager.current_transaction
|
27
|
+
end
|
28
|
+
|
29
|
+
def current_transaction_state
|
30
|
+
current_transaction.try(:state)
|
31
|
+
end
|
32
|
+
|
33
|
+
def current_accountability_context
|
34
|
+
AccountabilityContext.current
|
35
|
+
end
|
36
|
+
|
37
|
+
def current_agent
|
38
|
+
current_accountability_context.try(:agent)
|
39
|
+
end
|
40
|
+
|
41
|
+
def with_accountability(agent, &block)
|
42
|
+
execution = nil
|
43
|
+
accountability_context = AccountabilityContext.new(agent)
|
44
|
+
|
45
|
+
is_using_callbacks = root_transaction.joinable?
|
46
|
+
|
47
|
+
root_transaction.add_record RecordCallbackProxy.new accountability_context if is_using_callbacks
|
48
|
+
|
49
|
+
accountability_context.within do
|
50
|
+
execution = Execution.new(&block)
|
51
|
+
end
|
52
|
+
|
53
|
+
accountability_context.finalize! unless is_using_callbacks
|
54
|
+
|
55
|
+
execution.try(:value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def without_accountability(&block)
|
59
|
+
with_accountability(AnonymousAgent.instance, &block)
|
60
|
+
end
|
61
|
+
end
|
5
62
|
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'hipaapotamus/accountability_error'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
class AccountabilityContext
|
5
|
+
THREAD_STORAGE_KEY = :hipaapotamus_active_accountability_context
|
6
|
+
|
7
|
+
attr_reader :agent, :parent_accountability_context, :progenitor_actions
|
8
|
+
|
9
|
+
def initialize(agent)
|
10
|
+
raise AccountabilityError, 'Cannot create AccountabilityContext without a valid Agent' unless agent.is_a? Agent
|
11
|
+
|
12
|
+
@agent = agent
|
13
|
+
@open = true
|
14
|
+
@finalized = false
|
15
|
+
|
16
|
+
within { yield(self) } if block_given?
|
17
|
+
end
|
18
|
+
|
19
|
+
def open?
|
20
|
+
@open
|
21
|
+
end
|
22
|
+
|
23
|
+
# noinspection RubyArgCount
|
24
|
+
def record_action(protected, action_type, transactional = false)
|
25
|
+
action = Action.new(
|
26
|
+
agent: agent,
|
27
|
+
protected: protected,
|
28
|
+
action_type: action_type,
|
29
|
+
source_transaction_state: Hipaapotamus.current_transaction_state,
|
30
|
+
is_transactional: transactional,
|
31
|
+
performed_at: DateTime.now
|
32
|
+
)
|
33
|
+
|
34
|
+
actions << action
|
35
|
+
end
|
36
|
+
|
37
|
+
def finalized?
|
38
|
+
@finalized
|
39
|
+
end
|
40
|
+
|
41
|
+
def finalize!
|
42
|
+
raise(AccountabilityError, 'AccountabilityContext is open') if open?
|
43
|
+
raise(AccountabilityError, 'AccountabilityContext is finalized') if finalized?
|
44
|
+
|
45
|
+
Action.bulk_insert(log_worthy_actions) if root?
|
46
|
+
|
47
|
+
@finalized = true
|
48
|
+
end
|
49
|
+
|
50
|
+
def root?
|
51
|
+
parent_accountability_context.nil?
|
52
|
+
end
|
53
|
+
|
54
|
+
def within
|
55
|
+
raise(AccountabilityError, 'AccountabilityContext is not open') unless open?
|
56
|
+
@open = false
|
57
|
+
|
58
|
+
@parent_accountability_context = Thread.current[THREAD_STORAGE_KEY]
|
59
|
+
Thread.current[THREAD_STORAGE_KEY] = self
|
60
|
+
|
61
|
+
begin
|
62
|
+
|
63
|
+
yield(self)
|
64
|
+
ensure
|
65
|
+
Thread.current[THREAD_STORAGE_KEY] = @parent_accountability_context
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
protected
|
70
|
+
|
71
|
+
def actions
|
72
|
+
raise(AccountabilityError, 'AccountabilityContext is open') if open?
|
73
|
+
|
74
|
+
@actions ||= if root?
|
75
|
+
[]
|
76
|
+
else
|
77
|
+
parent_accountability_context.actions
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def log_worthy_actions
|
84
|
+
actions.select(&:log_worthy?)
|
85
|
+
end
|
86
|
+
|
87
|
+
class << self
|
88
|
+
def current
|
89
|
+
Thread.current[THREAD_STORAGE_KEY]
|
90
|
+
end
|
91
|
+
|
92
|
+
def current!
|
93
|
+
current || raise(AccountabilityError, 'Not within an AccountabilityContext')
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
module AccountableController
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
included do
|
8
|
+
around_action :wrap_in_accountability_context
|
9
|
+
rescue_from AccountabilityError, with: :agent_not_authorized
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def current_agent
|
15
|
+
current_user
|
16
|
+
end
|
17
|
+
|
18
|
+
def wrap_in_accountability_context
|
19
|
+
Hipaapotamus.with_accountability(current_agent) do
|
20
|
+
yield
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def agent_not_authorized(accountability_error)
|
25
|
+
render text: accountability_error.to_s, status: 401
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
class Action < ActiveRecord::Base
|
5
|
+
self.table_name = 'hipaapotamus_actions'
|
6
|
+
|
7
|
+
attr_accessor :source_transaction_state
|
8
|
+
|
9
|
+
enum action_type: { access: 0, creation: 1, modification: 2, destruction: 3,
|
10
|
+
attempted_access: 4, attempted_creation: 5, attempted_modification: 6, attempted_destruction: 7 }
|
11
|
+
|
12
|
+
def transactional?
|
13
|
+
is_transactional
|
14
|
+
end
|
15
|
+
|
16
|
+
def log_worthy?
|
17
|
+
persisted? || !transactional? || source_transaction_state.committed?
|
18
|
+
end
|
19
|
+
|
20
|
+
def agent_class
|
21
|
+
agent_type.try(:constantize)
|
22
|
+
end
|
23
|
+
|
24
|
+
def agent
|
25
|
+
if agent_class < Singleton
|
26
|
+
agent_class.instance
|
27
|
+
else
|
28
|
+
agent_class.find(agent_id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def agent=(agent)
|
33
|
+
if agent.is_a? Singleton
|
34
|
+
self.agent_id = nil
|
35
|
+
else
|
36
|
+
self.agent_id = agent.id
|
37
|
+
end
|
38
|
+
|
39
|
+
self.agent_type = agent.class.name
|
40
|
+
end
|
41
|
+
|
42
|
+
def protected_class
|
43
|
+
protected_type.try(:constantize)
|
44
|
+
end
|
45
|
+
|
46
|
+
def protected
|
47
|
+
@protected ||= protected_class.new.tap do |protected|
|
48
|
+
if protected_id.present?
|
49
|
+
protected.id = protected_id
|
50
|
+
end
|
51
|
+
|
52
|
+
if protected_attributes.present?
|
53
|
+
protected.assign_attributes protected_attributes
|
54
|
+
end
|
55
|
+
|
56
|
+
protected.authorize_access!
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def protected=(protected)
|
61
|
+
self.protected_id = protected.try(:id)
|
62
|
+
self.protected_type = protected.try(:class).try(:name)
|
63
|
+
self.protected_attributes = protected.try(:attributes)
|
64
|
+
|
65
|
+
@protected = protected
|
66
|
+
end
|
67
|
+
|
68
|
+
def protected_attributes
|
69
|
+
JSON.parse(serialized_protected_attributes) if serialized_protected_attributes.present?
|
70
|
+
end
|
71
|
+
|
72
|
+
def protected_attributes=(protected_attributes)
|
73
|
+
self.serialized_protected_attributes = protected_attributes.try(:to_json)
|
74
|
+
end
|
75
|
+
|
76
|
+
validate :not_changed
|
77
|
+
validates :agent_type, :protected_type, :protected_attributes, :action_type, :performed_at, presence: true
|
78
|
+
|
79
|
+
scope :with_protected, -> (protected) { where(protected_type: protected.class.name, protected_id: protected.id) }
|
80
|
+
|
81
|
+
class << self
|
82
|
+
def bulk_insert(actions)
|
83
|
+
if actions.length > 0
|
84
|
+
actions.each do |action|
|
85
|
+
raise ActiveRecord::RecordInvalid, 'unable to modify existing actions' unless action.new_record?
|
86
|
+
raise ActiveRecord::RecordInvalid, action.errors.full_messages.to_sentence unless action.valid?
|
87
|
+
end
|
88
|
+
|
89
|
+
attributeses = actions.map(&:attributes)
|
90
|
+
|
91
|
+
now = DateTime.now
|
92
|
+
attributeses.each { |attributes| attributes['created_at'] = now } if self.column_names.include?('created_at')
|
93
|
+
attributeses.each { |attributes| attributes['updated_at'] = now } if self.column_names.include?('updated_at')
|
94
|
+
|
95
|
+
uniq_keys = attributeses.map { |attributes| attributes.keys }.flatten(1).uniq.reject { |key| key == primary_key || key == primary_key.to_sym }
|
96
|
+
|
97
|
+
column_names = uniq_keys.map(&:to_s)
|
98
|
+
rows = attributeses.map { |attributes| uniq_keys.map { |key| attributes[key] } }
|
99
|
+
|
100
|
+
value_template = "(#{column_names.map{'?'}.join(', ')})"
|
101
|
+
|
102
|
+
value_clauses = rows.map { |values| sanitize_sql_array([value_template, *values]) }
|
103
|
+
values_clause = value_clauses.join(', ')
|
104
|
+
|
105
|
+
column_clauses = column_names.map { |column_name| connection.quote_column_name(column_name) }
|
106
|
+
columns_clause = "#{connection.quote_column_name(table_name)} (#{column_clauses.join(', ')})"
|
107
|
+
|
108
|
+
insert_statement = "INSERT INTO #{columns_clause} VALUES #{values_clause};"
|
109
|
+
|
110
|
+
connection.execute(insert_statement)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def not_changed
|
118
|
+
unless new_record?
|
119
|
+
self.errors.add(:action, 'cannot be changed')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'active_support/concern'
|
2
|
+
require 'hipaapotamus/accountability_context'
|
3
|
+
|
4
|
+
module Hipaapotamus
|
5
|
+
module Agent
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
def with_accountability(&block)
|
9
|
+
Hipaapotamus.with_accountability(self, &block)
|
10
|
+
end
|
11
|
+
|
12
|
+
def hipaapotamus_display_name
|
13
|
+
"#{self.class.name}(id=#{id})" rescue self.class.name
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Hipaapotamus
|
2
|
+
class Execution
|
3
|
+
def initialize
|
4
|
+
begin
|
5
|
+
@value = yield
|
6
|
+
@raised = false
|
7
|
+
rescue StandardError => value
|
8
|
+
@value = value
|
9
|
+
@raised = true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def raised?
|
14
|
+
@raised
|
15
|
+
end
|
16
|
+
|
17
|
+
def value
|
18
|
+
if raised?
|
19
|
+
raise @value
|
20
|
+
else
|
21
|
+
@value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'hipaapotamus/accountability_error'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
class Policy
|
5
|
+
attr_reader :agent, :protected
|
6
|
+
|
7
|
+
def initialize(agent, protected)
|
8
|
+
@agent, @protected = agent, protected
|
9
|
+
end
|
10
|
+
|
11
|
+
def authorized?(action)
|
12
|
+
SystemAgent === agent || try(:"#{action}?")
|
13
|
+
end
|
14
|
+
|
15
|
+
def authorize!(action)
|
16
|
+
authorized?(action) || raise(AccountabilityError, "#{agent.hipaapotamus_display_name} does not have #{action} privileges to #{protected.hipaapotamus_display_name}")
|
17
|
+
end
|
18
|
+
|
19
|
+
def creation?
|
20
|
+
false
|
21
|
+
end
|
22
|
+
|
23
|
+
def access?
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
def modification?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def destruction?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
class << self
|
36
|
+
def authorize!(agent, protected, action)
|
37
|
+
new(agent, protected).authorize!(action)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'hipaapotamus/accountability_error'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
module Protected
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
|
7
|
+
class_methods do
|
8
|
+
def policy_class_name
|
9
|
+
@policy_class_name ||= "#{name}Policy"
|
10
|
+
end
|
11
|
+
|
12
|
+
def policy_class
|
13
|
+
if policy_class_name
|
14
|
+
@policy_class ||= policy_class_name.constantize
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def policy_class!
|
19
|
+
policy_class || raise(AccountabilityError, "Could not find the policy class for #{name}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
included do
|
24
|
+
delegate :policy_class, :policy_class!, to: :class
|
25
|
+
|
26
|
+
after_initialize :authorize_access!, unless: :new_record?
|
27
|
+
after_create :authorize_creation!
|
28
|
+
after_update :authorize_modification!
|
29
|
+
after_destroy :authorize_destruction!
|
30
|
+
end
|
31
|
+
|
32
|
+
def hipaapotamus_display_name
|
33
|
+
if new_record?
|
34
|
+
"a new #{self.class.name}"
|
35
|
+
else
|
36
|
+
|
37
|
+
"#{self.class.name}(#{self.class.primary_key}=#{self[self.class.primary_key]})"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def authorize_access!
|
42
|
+
accountability_context = AccountabilityContext.current!
|
43
|
+
|
44
|
+
begin
|
45
|
+
policy_class!.authorize!(accountability_context.agent, self, :access)
|
46
|
+
|
47
|
+
accountability_context.record_action(self, :access)
|
48
|
+
rescue AccountabilityError => error
|
49
|
+
accountability_context.record_action(self, :attempted_access)
|
50
|
+
|
51
|
+
raise error
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def authorize_creation!
|
56
|
+
accountability_context = AccountabilityContext.current!
|
57
|
+
|
58
|
+
begin
|
59
|
+
policy_class!.authorize!(accountability_context.agent, self, :creation)
|
60
|
+
|
61
|
+
accountability_context.record_action(self, :creation, true)
|
62
|
+
rescue AccountabilityError => error
|
63
|
+
accountability_context.record_action(self, :attempted_creation)
|
64
|
+
|
65
|
+
raise error
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def authorize_modification!
|
70
|
+
accountability_context = AccountabilityContext.current!
|
71
|
+
|
72
|
+
begin
|
73
|
+
policy_class!.authorize!(accountability_context.agent, self, :modification)
|
74
|
+
|
75
|
+
accountability_context.record_action(self, :modification, true)
|
76
|
+
rescue AccountabilityError => error
|
77
|
+
accountability_context.record_action(self, :attempted_modification)
|
78
|
+
|
79
|
+
raise error
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def authorize_destruction!
|
84
|
+
accountability_context = AccountabilityContext.current!
|
85
|
+
|
86
|
+
begin
|
87
|
+
policy_class!.authorize!(accountability_context.agent, self, :destruction)
|
88
|
+
|
89
|
+
accountability_context.record_action(self, :destruction, true)
|
90
|
+
rescue AccountabilityError => error
|
91
|
+
accountability_context.record_action(self, :attempted_destruction)
|
92
|
+
|
93
|
+
raise error
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
|
3
|
+
module Hipaapotamus
|
4
|
+
class RecordCallbackProxy
|
5
|
+
attr_reader :accountability_context
|
6
|
+
|
7
|
+
def initialize(accountability_context)
|
8
|
+
@accountability_context = accountability_context
|
9
|
+
end
|
10
|
+
|
11
|
+
def rolledback!(*args)
|
12
|
+
accountability_context.finalize!
|
13
|
+
end
|
14
|
+
|
15
|
+
def before_committed!(*args)
|
16
|
+
# NOOP
|
17
|
+
end
|
18
|
+
|
19
|
+
def committed!(*args)
|
20
|
+
accountability_context.finalize!
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_to_transaction
|
24
|
+
ActiveRecord::Base.connection.add_transaction_record(self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/hipaapotamus/version.rb
CHANGED
metadata
CHANGED
@@ -1,15 +1,44 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: hipaapotamus
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alec Larsen
|
8
|
+
- Jacob Lee
|
8
9
|
autorequire:
|
9
|
-
bindir:
|
10
|
+
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date: 2015-11
|
12
|
+
date: 2015-12-11 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.1'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.1'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: activesupport
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '4.1'
|
35
|
+
type: :runtime
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '4.1'
|
13
42
|
- !ruby/object:Gem::Dependency
|
14
43
|
name: bundler
|
15
44
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,17 +73,58 @@ dependencies:
|
|
44
73
|
requirements:
|
45
74
|
- - "~>"
|
46
75
|
- !ruby/object:Gem::Version
|
47
|
-
version: '3.
|
76
|
+
version: '3.3'
|
48
77
|
type: :development
|
49
78
|
prerelease: false
|
50
79
|
version_requirements: !ruby/object:Gem::Requirement
|
51
80
|
requirements:
|
52
81
|
- - "~>"
|
53
82
|
- !ruby/object:Gem::Version
|
54
|
-
version: '3.
|
83
|
+
version: '3.3'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: pry
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
version: '0'
|
91
|
+
type: :development
|
92
|
+
prerelease: false
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
94
|
+
requirements:
|
95
|
+
- - ">="
|
96
|
+
- !ruby/object:Gem::Version
|
97
|
+
version: '0'
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: sqlite3
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - '='
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 1.3.11
|
105
|
+
type: :development
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - '='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 1.3.11
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: database_cleaner
|
114
|
+
requirement: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - '='
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: 1.0.1
|
119
|
+
type: :development
|
120
|
+
prerelease: false
|
121
|
+
version_requirements: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - '='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 1.0.1
|
55
126
|
description:
|
56
|
-
email:
|
57
|
-
- aleclarsen42@gmail.com
|
127
|
+
email:
|
58
128
|
executables: []
|
59
129
|
extensions: []
|
60
130
|
extra_rdoc_files: []
|
@@ -66,10 +136,21 @@ files:
|
|
66
136
|
- LICENSE.txt
|
67
137
|
- README.md
|
68
138
|
- Rakefile
|
69
|
-
- bin/console
|
70
|
-
- bin/setup
|
71
139
|
- hipaapotamus.gemspec
|
140
|
+
- lib/generators/hipaapotamus/install_generator.rb
|
141
|
+
- lib/generators/hipaapotamus/templates/create_hipaapotamus_actions.rb
|
72
142
|
- lib/hipaapotamus.rb
|
143
|
+
- lib/hipaapotamus/accountability_context.rb
|
144
|
+
- lib/hipaapotamus/accountability_error.rb
|
145
|
+
- lib/hipaapotamus/accountable_controller.rb
|
146
|
+
- lib/hipaapotamus/action.rb
|
147
|
+
- lib/hipaapotamus/agent.rb
|
148
|
+
- lib/hipaapotamus/anonymous_agent.rb
|
149
|
+
- lib/hipaapotamus/execution.rb
|
150
|
+
- lib/hipaapotamus/policy.rb
|
151
|
+
- lib/hipaapotamus/protected.rb
|
152
|
+
- lib/hipaapotamus/record_callback_proxy.rb
|
153
|
+
- lib/hipaapotamus/system_agent.rb
|
73
154
|
- lib/hipaapotamus/version.rb
|
74
155
|
homepage: https://github.com/anarchocurious/hipaapotamus
|
75
156
|
licenses:
|
@@ -81,9 +162,9 @@ require_paths:
|
|
81
162
|
- lib
|
82
163
|
required_ruby_version: !ruby/object:Gem::Requirement
|
83
164
|
requirements:
|
84
|
-
- - "
|
165
|
+
- - "~>"
|
85
166
|
- !ruby/object:Gem::Version
|
86
|
-
version: '0'
|
167
|
+
version: '2.0'
|
87
168
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
88
169
|
requirements:
|
89
170
|
- - ">="
|
@@ -96,4 +177,3 @@ signing_key:
|
|
96
177
|
specification_version: 4
|
97
178
|
summary: Hipaapotamus is an amazing gem for amazing people
|
98
179
|
test_files: []
|
99
|
-
has_rdoc:
|
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "hipaapotamus"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start
|