-
Notifications
You must be signed in to change notification settings - Fork 6
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
Check for common permissions issues with scratch area #765
Changes from 11 commits
6adfc2f
0afde5a
7846c68
c529b2b
91f7618
6fa4bf3
b3f9a7b
c114dbf
d53079f
6fa9b0e
928c7b0
d4250b8
9f0ede8
8b7dc04
a9d6b74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import stat | ||
from pathlib import Path | ||
|
||
|
||
def is_sgid_set(path: Path) -> bool: | ||
"""Check if the SGID bit is set so that new files created | ||
under a directory owned by a group are owned by that same group. | ||
|
||
See https://www.redhat.com/en/blog/suid-sgid-sticky-bit | ||
|
||
Args: | ||
path: Path to the file to check | ||
|
||
Returns: | ||
bool: True if the SGID bit is set | ||
""" | ||
|
||
mask = path.stat().st_mode | ||
return bool(mask & stat.S_ISGID) | ||
|
||
|
||
def get_owner_gid(path: Path) -> int: | ||
"""Get the GID of the owner of a file | ||
|
||
Args: | ||
path: Path to the file to check | ||
|
||
Returns: | ||
bool: The GID of the file owner | ||
""" | ||
|
||
return path.stat().st_gid |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,15 +10,25 @@ | |
|
||
from blueapi.cli.scratch import ensure_repo, scratch_install, setup_scratch | ||
from blueapi.config import ScratchConfig, ScratchRepository | ||
from blueapi.utils import get_owner_gid | ||
|
||
|
||
@pytest.fixture | ||
def directory_path() -> Generator[Path]: | ||
def directory_path_no_permissions() -> Generator[Path]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. How about There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Or keep them all explicit (if a bit verbose) with (I am aware this is getting pretty picky though - definitely not something that would block the PR) |
||
temporary_directory = TemporaryDirectory() | ||
yield Path(temporary_directory.name) | ||
temporary_directory.cleanup() | ||
|
||
|
||
@pytest.fixture | ||
def directory_path(directory_path_no_permissions: Path) -> Path: | ||
os.chmod( | ||
directory_path_no_permissions, | ||
os.stat(directory_path_no_permissions).st_mode + stat.S_ISGID, | ||
) | ||
return directory_path_no_permissions | ||
|
||
|
||
@pytest.fixture | ||
def file_path(directory_path: Path) -> Generator[Path]: | ||
file_path = directory_path / str(uuid.uuid4()) | ||
|
@@ -149,6 +159,40 @@ def test_setup_scratch_fails_on_non_directory_root( | |
setup_scratch(config) | ||
|
||
|
||
def test_setup_scratch_fails_on_non_sgid_root( | ||
directory_path_no_permissions: Path, | ||
): | ||
config = ScratchConfig(root=directory_path_no_permissions, repositories=[]) | ||
with pytest.raises(PermissionError): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TODO: Match raisesses |
||
setup_scratch(config) | ||
|
||
|
||
def test_setup_scratch_fails_on_wrong_gid( | ||
directory_path: Path, | ||
): | ||
config = ScratchConfig( | ||
root=directory_path, | ||
required_gid=12345, | ||
repositories=[], | ||
) | ||
assert get_owner_gid(directory_path) != 12345 | ||
with pytest.raises(PermissionError): | ||
setup_scratch(config) | ||
|
||
|
||
def test_setup_scratch_succeeds_on_required_gid( | ||
directory_path: Path, | ||
): | ||
os.chown(directory_path, uid=12345, gid=12345) | ||
config = ScratchConfig( | ||
root=directory_path, | ||
required_gid=12345, | ||
repositories=[], | ||
) | ||
assert get_owner_gid(directory_path) == 12345 | ||
setup_scratch(config) | ||
|
||
|
||
@patch("blueapi.cli.scratch.ensure_repo") | ||
@patch("blueapi.cli.scratch.scratch_install") | ||
def test_setup_scratch_iterates_repos( | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import stat | ||
from pathlib import Path | ||
from unittest.mock import Mock | ||
|
||
import pytest | ||
|
||
from blueapi.utils import get_owner_gid, is_sgid_set | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"bits", | ||
[ | ||
# Files | ||
0o10_0600, # -rw-------. | ||
0o10_0777, # -rwxrwxrwx. | ||
0o10_0000, # ----------. | ||
0o10_0644, # -rw-r--r--. | ||
0o10_0400, # -r--------. | ||
0o10_0666, # -rw-rw-rw-. | ||
0o10_0444, # -r--r--r--. | ||
# Directories | ||
0o04_0777, # drwxrwxrwx. | ||
0o04_0000, # d---------. | ||
0o04_0600, # drw-------. | ||
], | ||
ids=lambda p: f"{p:06o} ({stat.filemode(p)})", | ||
) | ||
def test_is_sgid_set_should_be_disabled(bits: int): | ||
assert not _mocked_is_sgid_set(bits) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"bits", | ||
[ | ||
# Files | ||
0o10_2777, # -rwxrwsrwx. | ||
0o10_2000, # ------S---. | ||
0o10_2644, # -rw-r-Sr--. | ||
0o10_2600, # -rw---S---. | ||
0o10_2400, # -r----S---. | ||
0o10_2666, # -rw-rwSrw-. | ||
0o10_2444, # -r--r-Sr--. | ||
# Directories | ||
0o04_2777, # drwxrwsrwx. | ||
0o04_2000, # d-----S---. | ||
0o04_2600, # drw---S---. | ||
], | ||
ids=lambda p: f"{p:06o} ({stat.filemode(p)})", | ||
) | ||
def test_is_sgid_set_should_be_enabled(bits: int): | ||
assert _mocked_is_sgid_set(bits) | ||
|
||
|
||
def _mocked_is_sgid_set(bits: int) -> bool: | ||
path = Mock(spec=Path) | ||
path.stat().st_mode = bits | ||
|
||
return is_sgid_set(path) | ||
|
||
|
||
def test_get_owner_gid(): | ||
path = Mock(spec=Path) | ||
path.stat().st_gid = 12345 | ||
|
||
assert get_owner_gid(path) == 12345 |
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.
Should this be removed from
ensure_repo
as well?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.
Hmm, there's another test which clones to a tempdir that only passes because of this line (because the test calls
ensure_repo
directly rather than invoking the CLI. Maybe the test should use mocks instead, although I do worry that sticking the umask set in the CLI isn't as robust as I thought. Might be better to pepper the code with someensure_correct_umask
function in strategic places...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 now think we'll have to mock out all the directories in these tests anyway because of a separate issue that is breaking the CI with
test_setup_scratch_succeeds_on_required_gid
. I'll make an issue for that.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.
#770