-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Build: allow to install packages with apt #8065
Conversation
0608e29
to
4ca6676
Compare
4ca6676
to
2435a4a
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks great. I think we should look a bit more closely at the security aspects of it, but I think my changes will at least help:
$ sudo apt-get install --yes -- --quiet test
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package --quiet
E: Unable to locate package test
@@ -64,7 +64,7 @@ This is to avoid typos and provide feedback on invalid configurations. | |||
|
|||
.. contents:: | |||
:local: | |||
:depth: 1 | |||
:depth: 3 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this will spam the TOC too much -- seems fine tho?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was between 2 or 3, but wanted to have build.apt_images
and sphing.builder
visible instead of just having sphinx
and build
, but no strong opinion.
return build | ||
|
||
def validate_apt_package(self, index): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I feel like we probably can be smarter about this. Can we not call apt after a --
, which is the shell way of handling the end of options?
Unless otherwise noted, each builtin command documented as accepting options preceded by ‘-’ accepts ‘--’ to signify the end of the options. The :, true, false, and test/[ builtins do not accept options and do not treat ‘--’ specially. The exit, logout, return, break, continue, let, and shift builtins accept and process arguments beginning with ‘-’ without requiring ‘--’. Other builtins that accept arguments but are not specified as accepting options interpret arguments beginning with ‘-’ as invalid options and require ‘--’ to prevent this interpretation.
We should still raise a validation error here, but I'd like to see additional restrictions for this built in.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point Eric.
Also, we need to take care of executing commands in the same line as well. Like,
apt-get install `touch /tmp/hello.txt`
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe python's subprocess command will handle this, but we should definitely be sure and have tests for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, subprocess
should handle this as long as you don't pass shell=True
. However, it's worth a test to make sure we don't ever mess that up.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we use subprocess, but the Docker client API (exec_create
), tho.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We don't use subprocess, we escape special chars in
readthedocs.org/readthedocs/doc_builder/environments.py
Lines 335 to 367 in 2435a4a
def get_wrapped_command(self): | |
""" | |
Wrap command in a shell and optionally escape special bash characters. | |
In order to set the current working path inside a docker container, we | |
need to wrap the command in a shell call manually. | |
Some characters will be interpreted as shell characters without | |
escaping, such as: ``pip install requests<0.8``. When passing | |
``escape_command=True`` in the init method this escapes a good majority | |
of those characters. | |
""" | |
bash_escape_re = re.compile( | |
r"([\t\ \!\"\#\$\&\'\(\)\*\:\;\<\>\?\@" | |
r'\[\\\]\^\`\{\|\}\~])', | |
) | |
prefix = '' | |
if self.bin_path: | |
prefix += 'PATH={}:$PATH '.format(self.bin_path) | |
command = ( | |
' '.join([ | |
bash_escape_re.sub(r'\\\1', part) if self.escape_command else part | |
for part in self.command | |
]) | |
) | |
return ( | |
"/bin/sh -c 'cd {cwd} && {prefix}{cmd}'".format( | |
cwd=self.cwd, | |
prefix=prefix, | |
cmd=command, | |
) | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tested defining packages in a malicious way? (e.g. trying to executed forbidden things). From this docstring it only happen under certain circumstances. Are we sure we are always calling this in that way?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
escape_command
defaults to true if that's what you mean.
readthedocs/projects/tasks.py
Outdated
user=settings.RTD_BUILD_SUPER_USER, | ||
) | ||
self.build_env.run( | ||
'apt-get', 'install', '-y', '-q', *packages, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should always use the long-version of arguments in code, to make it clearer what it does. Also add in the --
Ending
'apt-get', 'install', '-y', '-q', *packages, | |
# -- ends all command arguments to protect against users passing them | |
'apt-get', 'install', '--assume-yes', '--quiet', '--', *packages, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we want this to be quiet? I know the output is sort of long but it might help folks debug their own problems. I tested it with --quiet
and it's still fairly verbose but maybe a comment is appropriate on why.
I looked at what CircleCI does and they basically give users free reign to run arbitrary commands including apt-get install
. Our system seems quite a bit more locked down than that which seems fine. There's still some risk (could a user install some docker utilities and somehow access the host?) but I think as long as we're just installing built-in Debian packages and sanitizing the inputs really well, we should be OK.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we want this to be quiet? I know the output is sort of long but it might help folks debug their own problems. I tested it with --quiet and it's still fairly verbose but maybe a comment is appropriate on why.
-q
will suppress the progress bar not the output itself, -qq
will suppress everything.
@patch('readthedocs.projects.tasks.UpdateDocsTaskStep.setup_vcs', new=MagicMock) | ||
@patch.object(BuildEnvironment, 'run') | ||
@patch('readthedocs.doc_builder.config.load_config') | ||
def test_install_apt_packages(self, load_config, run): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like a lot of patch
es? Seems we're trying to stop the builds from processing, but I wonder if there's a cleaner way to do this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was able to remove one patch, but I think a cleaner way would require refactor the build task so we can mock self.build_env
, self.setup_env
and self.python_env
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me.
I also think we should take care a little deeper about security concerns here and make some extra QA for these cases.
return build | ||
|
||
def validate_apt_package(self, index): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point Eric.
Also, we need to take care of executing commands in the same line as well. Like,
apt-get install `touch /tmp/hello.txt`
Pinging @davidfischer here to chime in on the security angles of this. I think we've covered most of them (passing |
Co-authored-by: Eric Holscher <[email protected]>
'/', | ||
'.', | ||
] | ||
for start in invalid_starts: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I left this bc feels like it gives a nice error for usual mistakes, but isn't needed as the regex below already validates this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The changes here look good. I think we can ship this soon 👍
.. note:: | ||
|
||
When possible avoid installing Python packages using apt (``python3-numpy`` for example), | ||
:ref:`use pip or Conda instead <guides/reproducible-builds:pinning dependencies>`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM! 🎉 APT packages! 😻
return build | ||
|
||
def validate_apt_package(self, index): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you tested defining packages in a malicious way? (e.g. trying to executed forbidden things). From this docstring it only happen under certain circumstances. Are we sure we are always calling this in that way?
I'm probably trying this too soon (sorry am really excited about this feature 😄). Is this the right way to use it ( rapidsai/ucx-py#734 )? Is there some way to tell the package is being installed? If I just need to wait a bit, happy to wait 🙂 |
@jakirkham this change should be live by next tuesday in the afternoon. |
also, this will work only with the v2 of the conig file (you are using v1) https://docs.readthedocs.io/en/stable/config-file/v2.html version: 2
build:
apt_packages:
- libnuma1 |
Awesome! Thank you for working on this 🙏 Sorry for getting ahead of things here 😅 Will give this another try next week 🙂 |
@jakirkham and anyone else subscribed, this change is live now! Please let us know if you find any problems! |
Worked great! 😄 Thanks again 🙏 |
package-*
,package==2.x
) are allowedYou can test this locally with this branch from the test-builds https://github.com/readthedocs/test-builds/blob/use-apt/.readthedocs.yaml
Implements #8060
Related: #7566