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

WIP: Set permissions for atomic open_file() #1382

Closed
wants to merge 3 commits into from
Closed

WIP: Set permissions for atomic open_file() #1382

wants to merge 3 commits into from

Conversation

msmolens
Copy link
Contributor

@msmolens msmolens commented Aug 29, 2019

Previously, when open_file() was called with atomic=True, the target
file's permissions were always set to 0600, i.e. readable and writable
by the current user. These permissions come from the temporary file
created by tempfile.mkstemp().

This commit changes an atomic open_file() call to set the permissions
of the target file. If the target file already exists, then its current
permissions are retained. Otherwise, the permissions respect the current
umask.

Fixes #1376

Previously, when `open_file()` was called with `atomic=True`, the target
file's permissions were always set to 0600, i.e. readable and writable
by the current user. These permissions come from the temporary file
created by `tempfile.mkstemp()` [1].

This commit changes an atomic `open_file()` call to set the permissions
of the target file. If the target file already exists, then its current
permissions are retained. Otherwise, the permissions respect the current
umask.

[1] https://docs.python.org/3.7/library/tempfile.html#tempfile.mkstemp
@msmolens msmolens changed the title WIP: Set permissions for atomic open_file() Set permissions for atomic open_file() Aug 29, 2019
click/_compat.py Outdated
@@ -33,6 +34,12 @@ def _make_text_stream(stream, encoding, errors,
force_readable=force_readable,
force_writable=force_writable)

def _get_current_umask():
"""Get current umask."""
umask = os.umask(0)
Copy link
Contributor

Choose a reason for hiding this comment

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

This is, unfortunately, a potential security problem in the presence of multiple threads. (See https://bugs.python.org/issue21082.)

I’m not sure what to suggest instead, other than avoiding tempfile and writing the loop to safely create a fresh filename manually.

click/_compat.py Outdated
try:
permissions = stat.S_IMODE(os.stat(real_filename).st_mode)
except OSError:
permissions = 0o666 & ~_get_current_umask()
Copy link
Contributor

Choose a reason for hiding this comment

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

IMO for the fallback it's better to set here a default permission, like 0o666 or even more paranoidal 0o600. As @andersk right said it's not thread-safe.

Copy link
Contributor

@atugushev atugushev Sep 2, 2019

Choose a reason for hiding this comment

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

Or even better, just pass and let the permissions be None.

Copy link
Contributor

Choose a reason for hiding this comment

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

The default permissions if you didn’t specify atomic=True are implicitly 0o666 & ~umask, because that’s the default behavior of open() (no explicit umask computation is required). So it’s unexpected for atomic=True to result in different permissions. permissions = 0o666 is definitely too permissive, as chmod is not implicitly masked by umask, while permissions = 0o600 (or pass, which has the same effect) is too restrictive.

The underlying problem is that tempfile simply has the wrong permissions behavior for this use case. It’s designed for temporary files that will later be deleted, not for creating files with which to atomically replace other files.

Copy link
Contributor

Choose a reason for hiding this comment

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

You are right, anyways at least on save an existing file there will be correct permissions.

click/_compat.py Outdated Show resolved Hide resolved
Add vendored tempfile modules that are modified to create files with
standard permissions that respect the current umask. The built-in
tempfile modules create files that are readable and writable only by the
creating user.

Use the modules when creating a temporary file for atomic open_file().
@msmolens msmolens changed the title Set permissions for atomic open_file() WIP: Set permissions for atomic open_file() Sep 19, 2019
@davidism
Copy link
Member

I really appreciate the work you're putting into this, but this is way more complexity than I'm willing to add and support. Vendoring two copies of Python's tempfile module is not worth it. Does python-atomicwrites suggested in #320 have this issue? cc @untitaker

@msmolens
Copy link
Contributor Author

@davidism Thanks for the review. I agree that vendoring the modules is heavy-handed and not a great way forward. I'll close this pull request for now.

Note that python-atomicwrites also changes the permissions: untitaker/python-atomicwrites#42

@untitaker
Copy link
Contributor

Atomicwrites does NOT intentionally change permissions. Different permissions come from a result of how the file write is done.

@andersk
Copy link
Contributor

andersk commented Sep 19, 2019

I took a more minimalist stab at this myself in #1400. @msmolens I copied your tests from here; hope that’s fine.

@untitaker You can certainly argue that it “does not change permissions” because it internally works by creating a new file with different permissions and renaming it over the old file. But that argument is of little interest to a developer who wanted the permissions to stay the same.

@untitaker
Copy link
Contributor

untitaker commented Sep 19, 2019

I believe #1400 is a correct fix for this, and I am surprised that one exists. It's been a while since I looked into this, but somehow I came to the wrong conclusion that the umask was broken on file rename, not on open(). I would be happy to have this in atomicwrites. After all its stated motto is to solve this rabbit hole once and for all.

We could keep the impl in click simple and have atomicwrites as optional dep for "better" behavior.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Nov 13, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants