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.
@@ -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,9 @@
1
+ module InternalAffairs
2
+ module Patching
3
+ def patch_active_admin
4
+ ActiveAdmin::BaseController.class_eval do
5
+ include InternalAffairs::AuditedPage
6
+ end
7
+ end
8
+ end
9
+ 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,3 @@
1
+ module InternalAffairs
2
+ VERSION = '1.0.0'
3
+ 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