Skip to content

Commit 0624248

Browse files
committed
Fix CI
1 parent 9b1a3d1 commit 0624248

File tree

3 files changed

+89
-24
lines changed

3 files changed

+89
-24
lines changed

Diff for: .github/workflows/ci.yml

+5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ jobs:
4444
- name: Install packages
4545
run: sudo apt-get update && sudo apt-get install --no-install-recommends -y google-chrome-stable curl libjemalloc2 libsqlite3-0 libsqlite3-0
4646

47+
- name: Create required directories
48+
run: |
49+
sudo mkdir -p /var/lib/rails-new-io/{home,config,cache,data,gems/bin,gems/specifications,gems/gems,gems/extensions,workspaces}
50+
sudo chmod -R 777 /var/lib/rails-new-io
51+
4752
- name: Checkout code
4853
uses: actions/checkout@v4
4954

Diff for: app/services/command_execution_service.rb

+54-22
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
require "open3"
22
require "timeout"
33
require "fileutils"
4+
require "pathname"
45

56
class CommandExecutionService
67
RAILS_GEN_ROOT = "/var/lib/rails-new-io/rails-env".freeze
8+
WORKSPACES_ROOT = "/var/lib/rails-new-io/workspaces".freeze
79
RUBY_VERSION = "3.4.1".freeze
810
RAILS_VERSION = "8.0.1".freeze
911
BUNDLER_VERSION = "2.6.3".freeze
@@ -54,6 +56,15 @@ class CommandExecutionService
5456
VALID_OPTIONS = /\A--?[a-z][\w-]*\z/ # Must start with letter after dash(es)
5557
MAX_TIMEOUT = 300 # 5 minutes
5658

59+
ALLOWED_PATHS = [
60+
RAILS_GEN_ROOT,
61+
WORKSPACES_ROOT,
62+
"/var/lib/rails-new-io/home",
63+
"/var/lib/rails-new-io/config",
64+
"/var/lib/rails-new-io/cache",
65+
"/var/lib/rails-new-io/data"
66+
].map { |path| Pathname.new(path).freeze }.freeze
67+
5768
class InvalidCommandError < StandardError
5869
attr_reader :metadata
5970

@@ -73,7 +84,9 @@ def initialize(generated_app, logger, command = nil)
7384

7485
def execute
7586
validate_command!
87+
validate_work_directory! if @work_dir # Check existing work_dir if set
7688
setup_work_directory
89+
validate_work_directory! # Validate again after setup
7790

7891
Timeout.timeout(MAX_TIMEOUT) do
7992
run_isolated_process
@@ -117,25 +130,41 @@ def validate_command!
117130
@logger.debug("Command validation successful", { command: @command })
118131
end
119132

133+
def validate_work_directory!
134+
raise InvalidCommandError, "Work directory not set up" unless @work_dir
135+
work_dir_path = Pathname.new(@work_dir)
136+
137+
# Allow test directories (those under /tmp or /var/folders) in test environment
138+
return if Rails.env.test? && (work_dir_path.to_s.start_with?("/tmp/") || work_dir_path.to_s.start_with?("/var/folders/"))
139+
140+
# Ensure the work directory is under an allowed path
141+
unless ALLOWED_PATHS.any? { |allowed| work_dir_path.to_s.start_with?(allowed.to_s) }
142+
@logger.error("Invalid work directory path", { work_dir: @work_dir })
143+
raise InvalidCommandError, "Invalid work directory path"
144+
end
145+
146+
raise InvalidCommandError, "Work directory does not exist" unless Dir.exist?(@work_dir)
147+
end
148+
120149
def setup_work_directory
150+
base_dir = Pathname.new(WORKSPACES_ROOT)
151+
121152
@work_dir = if @command.start_with?("rails new")
122-
base_dir = "/var/lib/rails-new-io/workspaces"
123153
FileUtils.mkdir_p(base_dir)
124-
workspace_dir_name = "workspace-#{Time.current.to_i}-#{SecureRandom.hex(4)}"
125154

126-
File.join(base_dir, workspace_dir_name).tap do |dir|
127-
FileUtils.mkdir_p(dir)
128-
@generated_app.update(workspace_path: dir)
129-
@logger.info("Created workspace directory", { workspace_path: @work_dir })
130-
end
131-
else
132-
File.join(@generated_app.workspace_path, @generated_app.name).tap do |dir|
133-
@logger.info("Using existing workspace directory", { workspace_path: dir })
134-
end
135-
end
155+
timestamp = Time.current.to_i.to_s
156+
random_hex = SecureRandom.hex(4)
157+
workspace_dir_name = [ "workspace", timestamp, random_hex ].join("-")
136158

137-
if !Dir.exist?(@work_dir)
138-
raise InvalidCommandError, "Work directory #{@work_dir} does not exist!"
159+
dir = base_dir.join(workspace_dir_name)
160+
FileUtils.mkdir_p(dir)
161+
@generated_app.update(workspace_path: dir.to_s)
162+
@logger.info("Created workspace directory", { workspace_path: dir.to_s })
163+
dir.to_s
164+
else
165+
dir = Pathname.new(@generated_app.workspace_path).join(@generated_app.name)
166+
@logger.info("Using existing workspace directory", { workspace_path: dir.to_s })
167+
dir.to_s
139168
end
140169
end
141170

@@ -161,16 +190,19 @@ def run_isolated_process
161190

162191
rails_cmd = "#{RAILS_GEN_ROOT}/gems/bin/rails"
163192

164-
command = if @command.start_with?("rails new")
165-
args = @command.split[2..-1].join(" ")
166-
"#{rails_cmd} new #{args}"
193+
command_args = if @command.start_with?("rails new")
194+
[ "new", *@command.split[2..-1] ]
167195
else
168196
# For other rails commands (like app:template), don't include 'rails' in the args
169-
"#{rails_cmd} #{@command.split[1..-1].join(' ')}"
197+
@command.split[1..-1]
170198
end
171199

200+
# Validate work directory one final time before execution
201+
validate_work_directory!
202+
options = { unsetenv_others: true, chdir: @work_dir }
203+
172204
Bundler.with_unbundled_env do
173-
execute_command(env, command, buffer, error_buffer)
205+
execute_command(env, [ rails_cmd, *command_args ], buffer, error_buffer, options)
174206
end
175207

176208
@work_dir
@@ -201,8 +233,8 @@ def env_for_command
201233
base_env
202234
end
203235

204-
def execute_command(env, command, buffer, error_buffer)
205-
Open3.popen3(env, command, chdir: @work_dir, unsetenv_others: true) do |stdin, stdout, stderr, wait_thr|
236+
def execute_command(env, command_with_args, buffer, error_buffer, options)
237+
Open3.popen3(env, *command_with_args, options) do |stdin, stdout, stderr, wait_thr|
206238
@pid = wait_thr&.pid
207239

208240
stdout_thread = Thread.new do
@@ -230,7 +262,7 @@ def execute_command(env, command, buffer, error_buffer)
230262
status: exit_status,
231263
output: output,
232264
error_buffer: error_buffer.join("<br>"),
233-
command: command,
265+
command: command_with_args.join(" "),
234266
directory: @work_dir,
235267
env: env
236268
})

Diff for: test/services/command_execution_service_test.rb

+30-2
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def teardown
299299
private
300300

301301
def mock_popen3(stdout, stderr, success: true, pid: 12345)
302-
lambda do |env, command, **options, &block|
302+
lambda do |env, *command_with_args, options, &block|
303303
mock_stdin = StringIO.new
304304
mock_stdout = StringIO.new(stdout)
305305
mock_stderr = StringIO.new(stderr)
@@ -414,8 +414,36 @@ def mock_popen3(stdout, stderr, success: true, pid: 12345)
414414
@service.execute
415415
end
416416

417-
assert_equal "Work directory /var/lib/rails-new-io/workspaces/workspace-1739965840-1a21373e does not exist!", error.message
417+
assert_equal "Work directory does not exist", error.message
418418
end
419419
end
420420
end
421+
422+
test "raises error when work directory is outside allowed paths" do
423+
# Set up a directory outside of allowed paths
424+
malicious_path = "/etc/some/path"
425+
@generated_app.update!(workspace_path: malicious_path)
426+
427+
# Override the test environment check to ensure the validation runs
428+
Rails.env.stubs(:test?).returns(false)
429+
430+
# Use a template command since it uses the existing workspace path
431+
service = CommandExecutionService.new(@generated_app, @logger, "rails app:template LOCATION=lib/templates/template.rb")
432+
433+
# Set the work_dir directly to trigger validation
434+
service.instance_variable_set(:@work_dir, malicious_path)
435+
436+
assert_difference -> { AppGeneration::LogEntry.count }, 1 do
437+
error = assert_raises(CommandExecutionService::InvalidCommandError) do
438+
service.send(:validate_work_directory!)
439+
end
440+
441+
assert_equal "Invalid work directory path", error.message
442+
end
443+
444+
log_entries = @generated_app.log_entries.order(created_at: :asc).last(1)
445+
assert log_entries[0].error?
446+
assert_equal "Invalid work directory path", log_entries[0].message
447+
assert_equal({ "work_dir" => malicious_path }, log_entries[0].metadata)
448+
end
421449
end

0 commit comments

Comments
 (0)