internal-affairs 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/.rspec +2 -0
- data/.rubocop.yml +497 -0
- data/Gemfile +4 -0
- data/README.md +40 -0
- data/Rakefile +39 -0
- data/internal-affairs.gemspec +37 -0
- data/lib/generators/internal_affairs/active_record_generator.rb +22 -0
- data/lib/generators/internal_affairs/generator_helper.rb +7 -0
- data/lib/generators/internal_affairs/templates/migration.rb +10 -0
- data/lib/internal-affairs.rb +1 -0
- data/lib/internal_affairs/api_utils.rb +123 -0
- data/lib/internal_affairs/approvable_model.rb +31 -0
- data/lib/internal_affairs/audited_page.rb +63 -0
- data/lib/internal_affairs/configuration.rb +14 -0
- data/lib/internal_affairs/noop_operation.rb +41 -0
- data/lib/internal_affairs/operation_manager.rb +21 -0
- data/lib/internal_affairs/patching.rb +9 -0
- data/lib/internal_affairs/pending_operation.rb +34 -0
- data/lib/internal_affairs/pending_operations_update_job.rb +28 -0
- data/lib/internal_affairs/version.rb +3 -0
- data/lib/internal_affairs.rb +26 -0
- metadata +367 -0
@@ -0,0 +1,123 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
module ApiUtils
|
3
|
+
extend self
|
4
|
+
|
5
|
+
class Error < StandardError
|
6
|
+
def initialize(_response)
|
7
|
+
@response = _response
|
8
|
+
@status = _response.status
|
9
|
+
|
10
|
+
super "IA api responded with #{@status}"
|
11
|
+
end
|
12
|
+
|
13
|
+
def status
|
14
|
+
@response.status
|
15
|
+
end
|
16
|
+
|
17
|
+
def json_body
|
18
|
+
JSON.parse @response.body
|
19
|
+
rescue JSON::ParserError
|
20
|
+
nil
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_log(user:, ip:, kind:, data:, resources: [])
|
25
|
+
data = {
|
26
|
+
user: user,
|
27
|
+
ip: ip,
|
28
|
+
kind: kind,
|
29
|
+
data: data,
|
30
|
+
resources: resources
|
31
|
+
}
|
32
|
+
|
33
|
+
response = post("/api/v1/logs", log: data)
|
34
|
+
OpenStruct.new response['log']
|
35
|
+
end
|
36
|
+
|
37
|
+
def create_operation(user:, name:, amount: nil, uuid: nil, resources: [])
|
38
|
+
data = {
|
39
|
+
user: user,
|
40
|
+
name: name,
|
41
|
+
resources: resources
|
42
|
+
}
|
43
|
+
|
44
|
+
data[:uuid] = uuid if uuid.present?
|
45
|
+
|
46
|
+
if amount.present?
|
47
|
+
data[:amount] = amount.amount
|
48
|
+
data[:currency] = amount.currency.iso_code
|
49
|
+
end
|
50
|
+
|
51
|
+
response = post("/api/v1/operations", operation: data)
|
52
|
+
OpenStruct.new response['operation']
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_operation(_id)
|
56
|
+
response = get("/api/v1/operations/#{_id}")
|
57
|
+
OpenStruct.new response['operation']
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def get(_path, _headers = {})
|
63
|
+
perform_request(:get, _path, _headers)
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_query(_path, _query, _headers = {})
|
67
|
+
get(_query.present? ? "#{_path}?#{_query.to_query}" : _path, _headers)
|
68
|
+
end
|
69
|
+
|
70
|
+
def post(_path, _data, _headers = {})
|
71
|
+
perform_request(:post, _path, _data, _headers)
|
72
|
+
end
|
73
|
+
|
74
|
+
def put(_path, _data, _headers = {})
|
75
|
+
perform_request(:put, _path, _data, _headers)
|
76
|
+
end
|
77
|
+
|
78
|
+
def delete(_path, _headers = {})
|
79
|
+
perform_request(:delete, _path, _headers)
|
80
|
+
end
|
81
|
+
|
82
|
+
def perform_request(_method, _path, _body = nil, _headers = {})
|
83
|
+
full_path = "#{config.host}#{_path}"
|
84
|
+
|
85
|
+
response = connection.public_send(_method, full_path) do |request|
|
86
|
+
request.headers.merge!(
|
87
|
+
"X-App-Id" => config.app_key_id,
|
88
|
+
"Accept" => "application/json"
|
89
|
+
)
|
90
|
+
|
91
|
+
if _body.present?
|
92
|
+
request.body = _body.to_json
|
93
|
+
request.headers["Content-Type"] = "application/json"
|
94
|
+
end
|
95
|
+
|
96
|
+
signer.sign request, config.app_key_secret
|
97
|
+
end
|
98
|
+
|
99
|
+
process_json_response response
|
100
|
+
end
|
101
|
+
|
102
|
+
def process_json_response(_response)
|
103
|
+
raise Error.new(_response) unless _response.success?
|
104
|
+
|
105
|
+
_response.body.empty? ? nil : JSON.parse(_response.body)
|
106
|
+
end
|
107
|
+
|
108
|
+
def connection
|
109
|
+
Faraday.new do |faraday|
|
110
|
+
faraday.adapter :patron
|
111
|
+
faraday.options.timeout = config.connection_timeout
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def signer
|
116
|
+
Authograph.signer
|
117
|
+
end
|
118
|
+
|
119
|
+
def config
|
120
|
+
InternalAffairs.config
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
module ApprovableModel
|
3
|
+
extend ::ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def load_from_operation_serialized_attributes(_id)
|
7
|
+
find(_id)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def attributes_for_operation_serialization
|
12
|
+
attributes[self.class.primary_key]
|
13
|
+
end
|
14
|
+
|
15
|
+
def approvable_user
|
16
|
+
raise NotImplementedError, 'approvable_user not implemented'
|
17
|
+
end
|
18
|
+
|
19
|
+
def approvable_operation
|
20
|
+
raise NotImplementedError, 'approvable_operation not implemented'
|
21
|
+
end
|
22
|
+
|
23
|
+
def approvable_resources
|
24
|
+
[]
|
25
|
+
end
|
26
|
+
|
27
|
+
def approvable_amount
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
module AuditedPage
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
METHODS_WITH_BODY = ['POST', 'PUT']
|
6
|
+
FORM_IGNORED_FIELDS = ['utf8', 'authenticity_token', 'commit']
|
7
|
+
|
8
|
+
def audit_page_action
|
9
|
+
yield
|
10
|
+
ensure
|
11
|
+
create_audit_log_or_fail_silently if InternalAffairs.config.audit_logs_enabled?
|
12
|
+
end
|
13
|
+
|
14
|
+
def create_audit_log_or_fail_silently
|
15
|
+
InternalAffairs::ApiUtils.create_log(
|
16
|
+
user: current_admin_user.email,
|
17
|
+
ip: audited_ip,
|
18
|
+
kind: 'request',
|
19
|
+
data: audited_data,
|
20
|
+
resources: audited_resources
|
21
|
+
)
|
22
|
+
rescue StandardError
|
23
|
+
nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def audited_ip
|
27
|
+
request.env["HTTP_CF_CONNECTING_IP"] || request.ip
|
28
|
+
end
|
29
|
+
|
30
|
+
def audited_data
|
31
|
+
r = "#{request.method} #{response.status} #{request.path}"
|
32
|
+
if METHODS_WITH_BODY.include?(request.method)
|
33
|
+
r += " #{request.request_parameters.except(*FORM_IGNORED_FIELDS).to_json}"
|
34
|
+
end
|
35
|
+
r
|
36
|
+
end
|
37
|
+
|
38
|
+
def audited_resources
|
39
|
+
resources = [
|
40
|
+
{ kind: 'url', path: request.path },
|
41
|
+
{ kind: 'admin_page', admin_controller: params[:controller], admin_action: params[:action] }
|
42
|
+
]
|
43
|
+
|
44
|
+
if !(resource_class <= ActiveAdmin::Page)
|
45
|
+
association_chain.each do |parent|
|
46
|
+
next unless parent.respond_to?(:to_global_id)
|
47
|
+
|
48
|
+
resources << { kind: 'object', global_id: parent.to_global_id.to_s }
|
49
|
+
end
|
50
|
+
|
51
|
+
if params[:id].present? && resource.respond_to?(:to_global_id)
|
52
|
+
resources << { kind: 'object', global_id: resource.to_global_id.to_s }
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
resources
|
57
|
+
end
|
58
|
+
|
59
|
+
included do
|
60
|
+
around_action :audit_page_action
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
class Configuration
|
3
|
+
attr_accessor :host, :app_key_id, :app_key_secret, :disable_audit_logs, :connection_timeout
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@disable_audit_logs = false
|
7
|
+
@connection_timeout = 1.0
|
8
|
+
end
|
9
|
+
|
10
|
+
def audit_logs_enabled?
|
11
|
+
!disable_audit_logs && host.present?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
class NoopOperation
|
3
|
+
attr_reader :attributes
|
4
|
+
|
5
|
+
def self.load_from_operation_serialized_attributes(_attributes)
|
6
|
+
new(_attributes)
|
7
|
+
end
|
8
|
+
|
9
|
+
def attributes_for_operation_serialization
|
10
|
+
attributes
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(_attributes)
|
14
|
+
@attributes = _attributes
|
15
|
+
end
|
16
|
+
|
17
|
+
def approvable_user
|
18
|
+
@attributes[:user]
|
19
|
+
end
|
20
|
+
|
21
|
+
def approvable_operation
|
22
|
+
@attributes[:operation]
|
23
|
+
end
|
24
|
+
|
25
|
+
def approvable_resources
|
26
|
+
@attributes[:resources] || []
|
27
|
+
end
|
28
|
+
|
29
|
+
def approvable_amount
|
30
|
+
@attributes[:amount]
|
31
|
+
end
|
32
|
+
|
33
|
+
def approve!
|
34
|
+
# do nothing
|
35
|
+
end
|
36
|
+
|
37
|
+
def reject!
|
38
|
+
# do nothing
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
module OperationManager
|
3
|
+
def request_operation_approval(_target)
|
4
|
+
uuid = SecureRandom.uuid
|
5
|
+
|
6
|
+
response = InternalAffairs::ApiUtils.create_operation(
|
7
|
+
uuid: uuid,
|
8
|
+
user: _target.approvable_user,
|
9
|
+
name: _target.approvable_operation,
|
10
|
+
amount: _target.approvable_amount,
|
11
|
+
resources: _target.approvable_resources
|
12
|
+
)
|
13
|
+
|
14
|
+
if response.state == 'approved'
|
15
|
+
_target.approve!
|
16
|
+
else
|
17
|
+
InternalAffairs::PendingOperation.create! operation_uuid: uuid, target: _target
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
class PendingOperation < ::ActiveRecord::Base
|
3
|
+
serialize :target_attributes
|
4
|
+
|
5
|
+
self.table_name = "internal_affairs_pending_operations"
|
6
|
+
|
7
|
+
validates :operation_uuid, :target_class, presence: true
|
8
|
+
|
9
|
+
def target=(_target)
|
10
|
+
# use separate class and attributes serialization to support non-ar cases
|
11
|
+
|
12
|
+
if _target.present?
|
13
|
+
self.target_class = _target.class.to_s
|
14
|
+
self.target_attributes = _target.attributes_for_operation_serialization
|
15
|
+
else
|
16
|
+
self.target_class = self.target_attributes = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
@target = _target
|
20
|
+
end
|
21
|
+
|
22
|
+
def target
|
23
|
+
@target ||= load_target
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def load_target
|
29
|
+
return nil if target_class.nil?
|
30
|
+
|
31
|
+
target_class.constantize.load_from_operation_serialized_attributes(target_attributes)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module InternalAffairs
|
2
|
+
class PendingOperationsUpdateJob < ::ActiveJob::Base
|
3
|
+
def perform
|
4
|
+
InternalAffairs::PendingOperation.all.find_each do |operation|
|
5
|
+
update_operation operation
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def update_operation(_operation)
|
12
|
+
operation = InternalAffairs::ApiUtils.get_operation _operation.operation_uuid
|
13
|
+
|
14
|
+
ActiveRecord::Base.transaction do
|
15
|
+
case operation.state
|
16
|
+
when 'approved'
|
17
|
+
_operation.target.approve!
|
18
|
+
_operation.destroy
|
19
|
+
when 'rejected'
|
20
|
+
_operation.target.reject!
|
21
|
+
_operation.destroy
|
22
|
+
end
|
23
|
+
end
|
24
|
+
rescue StandardError => e
|
25
|
+
Raven.capture_exception e
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'internal_affairs/api_utils'
|
2
|
+
require 'internal_affairs/approvable_model'
|
3
|
+
require 'internal_affairs/audited_page'
|
4
|
+
require 'internal_affairs/configuration'
|
5
|
+
require 'internal_affairs/noop_operation'
|
6
|
+
require 'internal_affairs/operation_manager'
|
7
|
+
require 'internal_affairs/patching'
|
8
|
+
require 'internal_affairs/pending_operation'
|
9
|
+
require 'internal_affairs/pending_operations_update_job'
|
10
|
+
require 'internal_affairs/version'
|
11
|
+
|
12
|
+
module InternalAffairs
|
13
|
+
extend InternalAffairs::Patching
|
14
|
+
extend InternalAffairs::OperationManager
|
15
|
+
|
16
|
+
def self.config
|
17
|
+
@@config
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.configure
|
21
|
+
@@config = Configuration.new
|
22
|
+
yield @@config
|
23
|
+
|
24
|
+
patch_active_admin
|
25
|
+
end
|
26
|
+
end
|