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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 55b4065b5a2334f7fec593dbb43d62558fcd2786cde6765ba60099c3c3ff0b03
4
- data.tar.gz: ec40d991bf17242d482846b8c2495b4747b0cf590c7a77031624af333b5dbb5a
3
+ metadata.gz: bf3f27387c51c6e72f363fa0f92a788ed62b57874e685bc26b58642d5b57ed64
4
+ data.tar.gz: 87d8c7ee8492972fcc9172714b8056f4bd6003842bdaa64c590b07744735517a
5
5
  SHA512:
6
- metadata.gz: 90d60f43668a322d66005702e3b079a751d979e65157265cf70ab2bfe1a56f873c9a5cf717cdbadcda75b0d503ef5a237f7b634a1a6e1245314268c41aa23b3f
7
- data.tar.gz: 9fa2bbce56a9db1ee0ed6fbf62eee6e455d167b8c7f4235fe111fa7c1fdc394f6365c13c10e0db2b2dea1e59144d1489fd9c95a1cfd923c42f0b7ff91a5c3c7c
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
- return Time.now.to_i + EXPIRATION_IN_SECONDS
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'
@@ -1,3 +1,3 @@
1
1
  module ForestLiana
2
- VERSION = "9.17.7"
2
+ VERSION = "9.18.0"
3
3
  end
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
 
@@ -1,3 +1,4 @@
1
1
  ForestLiana.env_secret = 'env_secret_test'
2
2
  ForestLiana.auth_secret = 'auth_secret_test'
3
3
  ForestLiana.application_url = 'http://localhost:3000'
4
+ ForestLiana.workflow_executor_url = 'http://workflow-executor.test:4001'
@@ -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.17.7
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