forest_liana 9.17.7 → 9.18.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 +4 -4
- data/app/controllers/forest_liana/workflow_executions_controller.rb +71 -0
- data/app/services/forest_liana/token.rb +2 -1
- data/config/routes.rb +6 -0
- data/lib/forest_liana/version.rb +1 -1
- data/lib/forest_liana.rb +1 -0
- data/spec/dummy/config/initializers/forest_liana.rb +1 -0
- data/spec/requests/workflow_executions_spec.rb +153 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bf3f27387c51c6e72f363fa0f92a788ed62b57874e685bc26b58642d5b57ed64
|
|
4
|
+
data.tar.gz: 87d8c7ee8492972fcc9172714b8056f4bd6003842bdaa64c590b07744735517a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e642957d6f0925fd6a2567c44c3fce1f386e85da1ebc0abfc1fac9c36c19ee5fdc38ed33430d8ad2fd4e7e1a4a72f64c3c7894c187bb3d8445c511bcd3c2eead
|
|
7
|
+
data.tar.gz: 66098b227f8abcd58e3e8da41954e560d8dae0e32ad8f4c995cb1f5daab4a2481af0c4b3c1f9e35bd7f7a816a68c826dc4df161f6b83eec78be1b592b4834896
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require 'httparty'
|
|
2
|
+
|
|
3
|
+
module ForestLiana
|
|
4
|
+
class WorkflowExecutionsController < ApplicationController
|
|
5
|
+
FORWARDED_HEADERS = %w[Authorization Cookie].freeze
|
|
6
|
+
UPSTREAM_TIMEOUT_IN_SECONDS = 10
|
|
7
|
+
UPSTREAM_ERRORS = [
|
|
8
|
+
HTTParty::Error,
|
|
9
|
+
SocketError,
|
|
10
|
+
Errno::ECONNREFUSED,
|
|
11
|
+
Net::OpenTimeout,
|
|
12
|
+
Net::ReadTimeout,
|
|
13
|
+
Timeout::Error
|
|
14
|
+
].freeze
|
|
15
|
+
|
|
16
|
+
def show
|
|
17
|
+
forward_to_executor(method: :get, suffix: '')
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def trigger
|
|
21
|
+
forward_to_executor(method: :post, suffix: '/trigger')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def forward_to_executor(method:, suffix:)
|
|
27
|
+
base = ForestLiana.workflow_executor_url
|
|
28
|
+
if base.blank?
|
|
29
|
+
head :not_found
|
|
30
|
+
return
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
url = "#{base.sub(%r{/+\z}, '')}/runs/#{params[:run_id]}#{suffix}"
|
|
34
|
+
response = HTTParty.send(
|
|
35
|
+
method,
|
|
36
|
+
url,
|
|
37
|
+
headers: forwarded_headers,
|
|
38
|
+
query: forwarded_query,
|
|
39
|
+
body: forwarded_body(method),
|
|
40
|
+
verify: Rails.env.production?,
|
|
41
|
+
timeout: UPSTREAM_TIMEOUT_IN_SECONDS
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
render json: response.parsed_response, status: response.code
|
|
45
|
+
rescue *UPSTREAM_ERRORS => e
|
|
46
|
+
Rails.logger.error("[ForestLiana] workflow executor proxy error: #{e.class}: #{e.message}")
|
|
47
|
+
render json: { error: 'workflow_executor_unreachable' }, status: :service_unavailable
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def forwarded_headers
|
|
51
|
+
base = { 'Content-Type' => 'application/json' }
|
|
52
|
+
FORWARDED_HEADERS.each_with_object(base) do |name, acc|
|
|
53
|
+
value = request.headers[name]
|
|
54
|
+
acc[name] = value if value.present?
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def forwarded_query
|
|
59
|
+
params
|
|
60
|
+
.except(:run_id, :controller, :action, :format)
|
|
61
|
+
.to_unsafe_h
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def forwarded_body(method)
|
|
65
|
+
return nil if method == :get
|
|
66
|
+
|
|
67
|
+
raw = request.raw_post
|
|
68
|
+
raw.presence
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -9,7 +9,8 @@ module ForestLiana
|
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
def self.expiration_in_seconds
|
|
12
|
-
|
|
12
|
+
# NOTICE: Cast to Integer so the JWT exp claim is an RFC 7519 NumericDate.
|
|
13
|
+
return Time.now.to_i + EXPIRATION_IN_SECONDS.to_i
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def self.create_token(user, rendering_id)
|
data/config/routes.rb
CHANGED
|
@@ -23,6 +23,12 @@ ForestLiana::Engine.routes.draw do
|
|
|
23
23
|
# Scopes
|
|
24
24
|
post '/scope-cache-invalidation' => 'scopes#invalidate_scope_cache'
|
|
25
25
|
|
|
26
|
+
# Workflow executor proxy (mounted only when ForestLiana.workflow_executor_url is set)
|
|
27
|
+
if ForestLiana.workflow_executor_url.present?
|
|
28
|
+
get '_internal/workflow-executions/:run_id' => 'workflow_executions#show'
|
|
29
|
+
post '_internal/workflow-executions/:run_id/trigger' => 'workflow_executions#trigger'
|
|
30
|
+
end
|
|
31
|
+
|
|
26
32
|
# Stripe Integration
|
|
27
33
|
get '(*collection)_stripe_payments' => 'stripe#payments'
|
|
28
34
|
get ':collection/:id/stripe_payments' => 'stripe#payments'
|
data/lib/forest_liana/version.rb
CHANGED
data/lib/forest_liana.rb
CHANGED
|
@@ -30,6 +30,7 @@ module ForestLiana
|
|
|
30
30
|
mattr_accessor :logger
|
|
31
31
|
mattr_accessor :reporter
|
|
32
32
|
mattr_accessor :skip_schema_update
|
|
33
|
+
mattr_accessor :workflow_executor_url
|
|
33
34
|
# TODO: Remove once lianas prior to 2.0.0 are not supported anymore.
|
|
34
35
|
mattr_accessor :names_old_overriden
|
|
35
36
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require 'rails_helper'
|
|
2
|
+
|
|
3
|
+
describe 'Workflow executor proxy', type: :request do
|
|
4
|
+
let(:run_id) { 'run_abc123' }
|
|
5
|
+
let(:executor_url) { 'http://workflow-executor.test:4001' }
|
|
6
|
+
let(:bearer_token) do
|
|
7
|
+
JWT.encode(
|
|
8
|
+
{
|
|
9
|
+
id: 38,
|
|
10
|
+
email: 'michael.kelso@that70.show',
|
|
11
|
+
first_name: 'Michael',
|
|
12
|
+
last_name: 'Kelso',
|
|
13
|
+
team: 'Operations',
|
|
14
|
+
rendering_id: 16,
|
|
15
|
+
exp: Time.now.to_i + 2.weeks.to_i,
|
|
16
|
+
permission_level: 'admin'
|
|
17
|
+
},
|
|
18
|
+
ForestLiana.auth_secret,
|
|
19
|
+
'HS256'
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
let(:auth_headers) do
|
|
23
|
+
{
|
|
24
|
+
'Accept' => 'application/json',
|
|
25
|
+
'Content-Type' => 'application/json',
|
|
26
|
+
'Authorization' => "Bearer #{bearer_token}",
|
|
27
|
+
'Cookie' => 'forest_session_token=session-xyz'
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
let(:executor_response) do
|
|
31
|
+
instance_double(
|
|
32
|
+
HTTParty::Response,
|
|
33
|
+
parsed_response: { 'id' => run_id, 'state' => 'pending' },
|
|
34
|
+
code: 200
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
before do
|
|
39
|
+
allow(ForestLiana::IpWhitelist).to receive(:retrieve) { true }
|
|
40
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_whitelist_retrieved) { true }
|
|
41
|
+
allow(ForestLiana::IpWhitelist).to receive(:is_ip_valid) { true }
|
|
42
|
+
|
|
43
|
+
allow(HTTParty).to receive(:get).and_return(executor_response)
|
|
44
|
+
allow(HTTParty).to receive(:post).and_return(executor_response)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
describe 'GET /forest/_internal/workflow-executions/:run_id' do
|
|
48
|
+
it 'forwards GET to the executor /runs/:run_id endpoint' do
|
|
49
|
+
get "/forest/_internal/workflow-executions/#{run_id}", params: { foo: 'bar' }, headers: auth_headers
|
|
50
|
+
|
|
51
|
+
expect(HTTParty).to have_received(:get).with(
|
|
52
|
+
"#{executor_url}/runs/#{run_id}",
|
|
53
|
+
hash_including(
|
|
54
|
+
headers: hash_including(
|
|
55
|
+
'Authorization' => "Bearer #{bearer_token}",
|
|
56
|
+
'Cookie' => 'forest_session_token=session-xyz'
|
|
57
|
+
),
|
|
58
|
+
query: hash_including('foo' => 'bar')
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns the executor status and body verbatim' do
|
|
64
|
+
get "/forest/_internal/workflow-executions/#{run_id}", headers: auth_headers
|
|
65
|
+
|
|
66
|
+
expect(response.status).to eq(200)
|
|
67
|
+
expect(JSON.parse(response.body)).to eq('id' => run_id, 'state' => 'pending')
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
it 'rejects unauthenticated requests with 401' do
|
|
71
|
+
get "/forest/_internal/workflow-executions/#{run_id}"
|
|
72
|
+
|
|
73
|
+
expect(response.status).to eq(401)
|
|
74
|
+
expect(HTTParty).not_to have_received(:get)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe 'POST /forest/_internal/workflow-executions/:run_id/trigger' do
|
|
79
|
+
let(:trigger_body) { { step: 'approve', value: 42 } }
|
|
80
|
+
|
|
81
|
+
it 'forwards POST to the executor /runs/:run_id/trigger endpoint with the body' do
|
|
82
|
+
post(
|
|
83
|
+
"/forest/_internal/workflow-executions/#{run_id}/trigger",
|
|
84
|
+
params: trigger_body.to_json,
|
|
85
|
+
headers: auth_headers
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
expect(HTTParty).to have_received(:post).with(
|
|
89
|
+
"#{executor_url}/runs/#{run_id}/trigger",
|
|
90
|
+
hash_including(
|
|
91
|
+
headers: hash_including('Authorization' => "Bearer #{bearer_token}"),
|
|
92
|
+
body: trigger_body.to_json
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'returns the executor response' do
|
|
98
|
+
post(
|
|
99
|
+
"/forest/_internal/workflow-executions/#{run_id}/trigger",
|
|
100
|
+
params: trigger_body.to_json,
|
|
101
|
+
headers: auth_headers
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
expect(response.status).to eq(200)
|
|
105
|
+
expect(JSON.parse(response.body)).to eq('id' => run_id, 'state' => 'pending')
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe 'when the executor returns an error status' do
|
|
110
|
+
let(:executor_response) do
|
|
111
|
+
instance_double(HTTParty::Response, parsed_response: { 'error' => 'invalid_step' }, code: 422)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
it 'forwards the executor status and body to the client' do
|
|
115
|
+
get "/forest/_internal/workflow-executions/#{run_id}", headers: auth_headers
|
|
116
|
+
|
|
117
|
+
expect(response.status).to eq(422)
|
|
118
|
+
expect(JSON.parse(response.body)).to eq('error' => 'invalid_step')
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe 'when the executor is unreachable' do
|
|
123
|
+
before do
|
|
124
|
+
allow(HTTParty).to receive(:get).and_raise(Errno::ECONNREFUSED.new('boom'))
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
it 'returns 503 service_unavailable' do
|
|
128
|
+
get "/forest/_internal/workflow-executions/#{run_id}", headers: auth_headers
|
|
129
|
+
|
|
130
|
+
expect(response.status).to eq(503)
|
|
131
|
+
expect(JSON.parse(response.body)).to eq('error' => 'workflow_executor_unreachable')
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe 'when ForestLiana.workflow_executor_url is blank' do
|
|
136
|
+
around do |example|
|
|
137
|
+
original = ForestLiana.workflow_executor_url
|
|
138
|
+
ForestLiana.workflow_executor_url = nil
|
|
139
|
+
example.run
|
|
140
|
+
ensure
|
|
141
|
+
ForestLiana.workflow_executor_url = original
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'returns 404 (controller-level guard for cases where routes were drawn but config was reset)' do
|
|
145
|
+
# Note: routes are mounted at boot based on workflow_executor_url being
|
|
146
|
+
# present. This test exercises the runtime guard inside the controller
|
|
147
|
+
# for scenarios where config is mutated after boot (e.g. tests).
|
|
148
|
+
get "/forest/_internal/workflow-executions/#{run_id}", headers: auth_headers
|
|
149
|
+
|
|
150
|
+
expect(response.status).to eq(404)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: forest_liana
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 9.
|
|
4
|
+
version: 9.18.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sandro Munda
|
|
@@ -250,6 +250,7 @@ files:
|
|
|
250
250
|
- app/controllers/forest_liana/smart_actions_controller.rb
|
|
251
251
|
- app/controllers/forest_liana/stats_controller.rb
|
|
252
252
|
- app/controllers/forest_liana/stripe_controller.rb
|
|
253
|
+
- app/controllers/forest_liana/workflow_executions_controller.rb
|
|
253
254
|
- app/deserializers/forest_liana/resource_deserializer.rb
|
|
254
255
|
- app/helpers/forest_liana/adapter_helper.rb
|
|
255
256
|
- app/helpers/forest_liana/application_helper.rb
|
|
@@ -448,6 +449,7 @@ files:
|
|
|
448
449
|
- spec/requests/resources_spec.rb
|
|
449
450
|
- spec/requests/stats_spec.rb
|
|
450
451
|
- spec/requests/test.ru
|
|
452
|
+
- spec/requests/workflow_executions_spec.rb
|
|
451
453
|
- spec/routing/routes_spec.rb
|
|
452
454
|
- spec/services/forest_liana/ability/ability_spec.rb
|
|
453
455
|
- spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb
|
|
@@ -760,6 +762,7 @@ test_files:
|
|
|
760
762
|
- spec/requests/resources_spec.rb
|
|
761
763
|
- spec/requests/stats_spec.rb
|
|
762
764
|
- spec/requests/test.ru
|
|
765
|
+
- spec/requests/workflow_executions_spec.rb
|
|
763
766
|
- spec/routing/routes_spec.rb
|
|
764
767
|
- spec/services/forest_liana/ability/ability_spec.rb
|
|
765
768
|
- spec/services/forest_liana/ability/permission/smart_action_checker_spec.rb
|