Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make send_file aware of user option #277

Merged
merged 6 commits into from Jan 18, 2019

Conversation

pocke
Copy link
Contributor

@pocke pocke commented Dec 27, 2018

Problem

user option does not work with file, remote_file and so on resources.
Probably other resouces have similar problems, but I've not confirm them yet. I describe the similar problems in the Cause section.

background

I'd like to execute itamae process with an ordinary user, but I'd like to put files that are owned by the root user.
When itamae needs the root user, I want to write user 'root' explicitly, and I want to use sudo command only if itamae needs the root user.

For instance, I'd like to use Itamae to provision a desktop computer for development. In this case, I'd like to put some files that is owned by an ordinary user (e.g. ~/.vimrc), and owned by the root user (e.g. /etc/hosts) too.
Unfortunately I cannot execute Itamae on the background.
I investigated this problem, and I describe this problem in this pull request.

Related issues

The first three issues mention the same problem, so I think we can close these issues by this pull request.
The last issue is a pull request for this problem, but I think the patch is not appropriate because the same reason as the sorah's comment. So we need another solution for this problem.

Reproduce

test.rb

file 'a' do
  user 'root'
  path '/a'
  content 'a'
end
$ itamae version
Itamae v1.10.2

$ itamae local test.rb
 INFO : Starting Itamae...
 INFO : Recipe: /tmp/tmp.LeJHTLb9B9/test.rb
[sudo] password for pocke: # <Enter the password>
Traceback (most recent call last):
	51: from /home/pocke/.rbenv/versions/trunk/bin/itamae:23:in `<main>'
	50: from /home/pocke/.rbenv/versions/trunk/bin/itamae:23:in `load'
	49: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/bin/itamae:4:in `<top (required)>'
	48: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/thor-0.20.3/lib/thor/base.rb:466:in `start'
	47: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/thor-0.20.3/lib/thor.rb:387:in `dispatch'
	46: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/thor-0.20.3/lib/thor/invocation.rb:126:in `invoke_command'
	45: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/thor-0.20.3/lib/thor/command.rb:27:in `run'
	44: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/cli.rb:37:in `local'
	43: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/cli.rb:137:in `run'
	42: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/runner.rb:14:in `run'
	41: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/runner.rb:61:in `run'
	40: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:57:in `run'
	39: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:57:in `each'
	38: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:58:in `block in run'
	37: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe.rb:64:in `run'
	36: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:13:in `event'
	35: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:29:in `_event_with_block'
	34: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe.rb:65:in `block in run'
	33: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/logger.rb:10:in `with_indent'
	32: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe.rb:66:in `block (2 levels) in run'
	31: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:57:in `run'
	30: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:57:in `each'
	29: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/recipe_children.rb:58:in `block in run'
	28: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:124:in `run'
	27: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:13:in `event'
	26: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:29:in `_event_with_block'
	25: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:127:in `block in run'
	24: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/logger.rb:19:in `with_indent_if'
	23: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:136:in `block (2 levels) in run'
	22: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:136:in `each'
	21: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:137:in `block (3 levels) in run'
	20: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:178:in `run_action'
	19: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:13:in `event'
	18: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/handler_proxy.rb:29:in `_event_with_block'
	17: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:188:in `block in run_action'
	16: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/logger.rb:19:in `with_indent_if'
	15: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/base.rb:190:in `block (2 levels) in run_action'
	14: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/file.rb:32:in `pre_action'
	13: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/resource/file.rb:191:in `send_tempfile'
	12: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/itamae-1.10.2/lib/itamae/backend.rb:109:in `send_file'
	11: from /home/pocke/.rbenv/versions/trunk/lib/ruby/gems/2.7.0/gems/specinfra-2.76.5/lib/specinfra/backend/exec.rb:25:in `send_file'
	10: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:418:in `cp'
	 9: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1555:in `fu_each_src_dest'
	 8: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1573:in `fu_each_src_dest0'
	 7: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1557:in `block in fu_each_src_dest'
	 6: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:419:in `block in cp'
	 5: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:492:in `copy_file'
	 4: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1385:in `copy_file'
	 3: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1385:in `open'
	 2: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1386:in `block in copy_file'
	 1: from /home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1386:in `open'
/home/pocke/.rbenv/versions/trunk/lib/ruby/2.7.0/fileutils.rb:1386:in `initialize': Permission denied @ rb_sysopen - /tmp/itamae_tmp/1545847140.9009516 (Errno::EACCES)

Sorah says "Why not using owner attribute instead of user?" in this comment ( #221 (comment) ), but the suggestion raises another error in this case.

test.rb

file 'a' do
  owner 'root'
  path '/a'
  content 'a'
end
$ itamae local test.rb
 INFO : Starting Itamae... 
 INFO : Recipe: /tmp/tmp.LeJHTLb9B9/test.rb
 INFO :   file[a] exist will change from 'false' to 'true'
 INFO :   file[a] modified will change from 'false' to 'true'
 INFO :   file[a] owner will be 'root'
 INFO :   diff:
 INFO :   --- /dev/null	2018-12-11 08:30:42.354322279 +0900
 INFO :   +++ /tmp/itamae_tmp/1545849545.8678365	2018-12-27 03:39:05.872209770 +0900
 INFO :   @@ -0,0 +1 @@
 INFO :   +a
 INFO :   \ No newline at end of file
ERROR :     stderr | chown: changing ownership of '/tmp/itamae_tmp/1545849545.8678365': Operation not permitted
ERROR :     Command `chown root /tmp/itamae_tmp/1545849545.8678365` failed. (exit status: 1)
ERROR :   file[a] Failed.

Itamae changes the owner of the file to specified user by owner option, which is the root user, but executed user is an ordinary user so the user does not have the permission to chown.

I think I need the following code for expected behaviour. But it also raises the same error as the first code.

file 'a' do
  user 'root' # need permission for chown this file
  owner 'root' # need to specify the owner
  group 'root' # need to specify the group
  path '/a'
  content 'a'
end

Cause

In short, because send_file is not aware of user option.

The error is occurred in lib/itamae/backend.rb:109 in send_file method.

@backend.send_file(src, dst)

It calls Specinfra::Backend::XXX#send_file. The XXX is Exec, SSH, Docker, and so on. When it is Exec, which Itamae::BackendisLocal, send_filemethod callsFileUtils.cp` method.
https://github.com/mizzy/specinfra/blob/d3c00a0076c7ef86d28e22ac545f69db7c031d70/lib/specinfra/backend/exec.rb#L24-L26

FileUtils.cp is a Ruby method, not an external command. So it is not aware of user option. It's simply called in an itamae process, so itamae does not change user.

By the way, I wrote " I describe the similar problems in the Cause section." in the first section of this pull request.
Itamae has similar code, so I guess there are similar problems.
For example, Backend#send_directory medhod also calls FileUtils method.

def send_directory(src, dst)
Itamae.logger.debug "Sending a directory from '#{src}' to '#{dst}'..."
unless ::File.exist?(src)
raise SourceNotExistError, "The directory '#{src}' doesn't exist."
end
unless ::File.directory?(src)
raise SourceNotExistError, "'#{src}' is not a directory."
end
@backend.send_directory(src, dst)
end

https://github.com/mizzy/specinfra/blob/d3c00a0076c7ef86d28e22ac545f69db7c031d70/lib/specinfra/backend/exec.rb#L28-L30
I also find receive_file method that is monkeypatched by Itamae.
class Exec < Base
def receive_file(from, to = nil)
if to
FileUtils.cp(from, to)
else
::File.read(from)
end
end
end

Maybe we need to fix them, but this pull request does not change them. It only focuses send_file method.
Because I do not use other than send_file method, so I do not have motivation to fix these method.

Solution

Use an external command for send_file method. See the diff!

Update 2019-01-16: This description is incomplete. Please read this comment also. #277 (comment)

Note

This patch does not add any test cases. I'm not sure how to test this patch. I think we should add an integration test case for this change, but the integration test works only with docker. So I cannot add an integration test case for itamae local command.
If you have a good solution for test, please tell me it. I'd like to write test for this problem.

I guess we can execute the integration test in the docker image, but with itamae local command also. For examle, docker run ubuntu:trusty -v $(pwd):/itamae /itamae/bin/itamae local ... (this command is simplified).

note: I've confirmed the patch works well in my environment.

@sue445
Copy link
Member

sue445 commented Dec 27, 2018

I think good, but I need to think other corner case for subclass of Itamae::Resource::File. (e.g. Itamae::Resource::RemoteFile and Itamae::Resource::Template )

Please wait a few days. 🙏

@pocke
Copy link
Contributor Author

pocke commented Dec 27, 2018

Thank you for your reviewing!

Please wait a few days.

No problem. I'm itamae beginner, so there's a possibility that I overlooked somethings.

@fuminori-ido
Copy link
Contributor

Dear all,

Regarding to the test, I think unit-test on Itamae::Resource::File is enough to cover this kind of issue so that I write the test below as spec/unit/lib/itamae/resource/file_spec.rb which works well as I expect(it fails on v1.10.2, but it passes on this PR).

Unfortunately, this requires 'SUDO' 😢. When PASSWD is set in sudoers config, sudo prompt appears every test execution... so that I didn't PR and just show the test here to discuss any better approach (although I set NOPASSWD in sudoers config so that I do not feel any stress on this, actually).

require 'itamae'

describe Itamae::Resource::File do
  describe 'when user attribute is different from execution user' do
    file_resource_path    = '/tmp/itamae_test_a'
    file_resource_content = 'a-content'

    subject(:file_resource) do
      runner  = Itamae::Runner.run([], :local, {log_level: 'info'})
      recipe  = Itamae::Recipe.new(runner, '/tmp/dummy_recipe.rb')

      described_class.new(recipe, 'a') do
        user    'nobody'
        path    file_resource_path
        content file_resource_content
      end
    end

    describe '@resource_name' do
      it { expect(subject.resource_name).to       eq 'a' } 
      it { expect(subject.attributes.content).to  eq file_resource_content } 
    end

    describe '#run' do
      FileUtils.rm_f(file_resource_path)
      it 'create' do
        subject.run
        expect(File).to exist(file_resource_path)
        expect(File.read(file_resource_path)).to  eq file_resource_content
      end
    end
  end
end

I need to think other corner case for subclass of Itamae::Resource::File. (e.g. Itamae::Resource::RemoteFile and Itamae::Resource::Template )

Agreed, but Itamae::Resource::File is a base class of RemoteFile and Template so that it could be good enough to workaround on this user-attr vs execution user issue. This issue is quite important for our project deployment. We overwrite send_file method with previous one to workaround this. I hope this issue will be fixed & released soon 😄 .

@pocke
Copy link
Contributor Author

pocke commented Jan 8, 2019

@fuminori-ido Thank you for your comment, and the test case!

Unfortunately, this requires 'SUDO'

Personaly it is not acceptable to me (NOT to itamae-kitchen team). It is stressful, and I think it is storange that test case requires sudo permission. If a test requires sudo permission, I will stop the test case by security reason.

I tried to execute the integration test on docker with itame local command. Because we can avoid the "sudo" problem by using docker.
But it is a bit of hard. We use ubuntu trusty for test, but trusty has Ruby 1.9.3 by default 🙃 . I think we need to create a docker image(s) for the testhing if run the test in docker.

@sue445
Copy link
Member

sue445 commented Jan 8, 2019

@fuminori-ido Thank you for test code. But this depends on the internal implementation of Itamae.
In this case, I prefer integration test rather than unit test.

Itamae::Resource::File is a base class of RemoteFile and Template so that it could be good enough to workaround on this user-attr vs execution user issue.

I think too. But I doesn't write and run test code yet.

I believe production code that passes the test code than my imagination.

@pocke
Copy link
Contributor Author

pocke commented Jan 13, 2019

I'm working on the integration test with itamae local command. #281

@pocke
Copy link
Contributor Author

pocke commented Jan 15, 2019

I added an integration test for ordinary user. It tests the following.

  • itamae local command is executed by an ordinary user.
  • Test File, RemoteFile, Template and HttpRequest resources
    • File resource and resources that inherit File
  • Test the following owners.
    • A file is owned by the execution user itself.
      • Without user, owner and group options.
    • A file is owned by the root user.
      • With user, owner and group options. All options have root as the value.
    • A file is owned by another ordinary user.
      • With user option that has root as the value, and owner and group options that have another ordinary user name as value.

I think it is better if the recipes/default.rb is tested with an ordinary user also.
But default.rb has many the root user dependent code, so I skipped it in this pull request.

Note

I found a regression, but personally I think it is not a bug.
The following recipe does not work with this patch, if the execution user is root.

file '/tmp/a' do
  user 'itamae'
  source "hello.txt"
end

log:

DEBUG :       Sending a file from '/tmp/itamae20190115-996-1lqzqll' to '/tmp/itamae_tmp/1547575550.5372958'...
DEBUG :       Executing `sudo -H -u itamae -- /bin/sh -c cd\ \~itamae\ \;\ cp\ -p\ /tmp/itamae20190115-996-1lqzqll\ /tmp/itamae_tmp/1547575550.5372958`...
DEBUG :         stderr | cp: cannot open '/tmp/itamae20190115-996-1lqzqll' for reading: Permission denied
ERROR :         Command `sudo -H -u itamae -- /bin/sh -c cd\ \~itamae\ \;\ cp\ -p\ /tmp/itamae20190115-996-1lqzqll\ /tmp/itamae_tmp/1547575550.5372958` failed. (exit status: 1)
ERROR :   file[/tmp/a] Failed.

Template and HttpRequest have the same problem probably, but RemoteFile does not have this problem.

It is a permission problem.
File resource creates a tempfile by the root user, and the permission is 600. It means users other than root cannot read the tempfile.
And the tempfile will be copied by user that is specified by the user option.
If the specified user is an ordinary user, the user cannot read the tempfile. So file resource raises an error.

begin
src = if content_file
content_file
else
f = Tempfile.open('itamae')
f.write(attributes.content)
f.close
f.path
end
@temppath = ::File.join(runner.tmpdir, Time.now.to_f.to_s)
if backend.is_a?(Itamae::Backend::Docker)
run_command(["mkdir", @temppath])
backend.send_file(src, @temppath, user: attributes.user)
@temppath = ::File.join(@temppath, ::File.basename(src))
else
run_command(["touch", @temppath])
run_specinfra(:change_file_mode, @temppath, '0600')
backend.send_file(src, @temppath, user: attributes.user)
end

However, personally I think it is acceptable. Because we can avoid this error with options. The following code works well.

# Use owner and group options instead of user option
file '/tmp/a' do
  owner 'itamae'
  group 'itamae'
  source "hello.txt"
end

I think specifying owner and group is better than user option in this case. And specifying owner and group is the right way in my opinion.
User expects it puts a file, owned by the specified user, and grouped into the specified group. I think user option is too implicit. user option specifies execution user. NOT owner and group.

However, maybe there are some recipes that will be broken by this pull request unfortunately.

What do you think about this regression? If you do not accept this regression, I'll rethink the solution to avoid this problem.

@pocke
Copy link
Contributor Author

pocke commented Jan 16, 2019

Sorry, I found another regression. I'll describe and fix the regression soon.

@pocke
Copy link
Contributor Author

pocke commented Jan 16, 2019

I've fixed the metioned both regressions. And add specs for the regressions.

Problem

The latter regression, which it haven't been described by me yet, is similar the former one. But it happens only if an ordinary user executes itamae.

file '/tmp/a' do
  user 'another_ordinary_user'
  owner 'another_ordinary_user'
  group 'another_ordinary_user'
end

If an ordinary user other than another_ordinary_user executes itamae, it fails. It is the same cause of the former regression. The user who executes process creates a tempfile with 600 permission, but the another_ordinary_user does not have read permission.

We can avoid the regression with specifing user 'root', but I think root user has too strong permission in this case. So I think we should fix this regression.

And I guess this regression will be an actual problem. For example, I can easily imagine the following case.

  • Itamae process is executed by itamae user.
  • Itamae puts /app/main.rb as app user.

The case does not work if the regression exists.

Solution

See 2f426bc.
It changes the users to read and write the tempfile.
To read the tempfile, itamae uses process execution user. Because the tempfile is created by process execution user, so the user can read the file.
To write the tempfile, itamae uses user spcecified user option.

I choose shell pipe to receive content of the tempfile between the users.

######

# Docker backend raises an error with `user` option, so it tests only on `itamae local`.
# After fix this error, please move this code and the spec to `default.rb`.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to add the following code to recipes/default.rb, but it does not work.
Because docker backend raises an error with this code.
Docker backend's send_file sends a file with the root user, and currently, it is not configurable, unfortunately.
I guess we can move the code to recipes/default.rb by fixing docker backend, but this pull request does not focus this problem. I guess this problem already exists before this pull request because I do not touch docker backend in this pull request.

Copy link
Member

@unasuke unasuke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❤️

@sue445 sue445 merged commit bf51e19 into itamae-kitchen:master Jan 18, 2019
@sue445
Copy link
Member

sue445 commented Jan 18, 2019

I'll release later

@pocke pocke deleted the user-option-file branch January 18, 2019 06:45
@pocke
Copy link
Contributor Author

pocke commented Jan 18, 2019

Thank you for your reviewing!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants