Skip to content

Fix permission handling in _setup_cookie_directory and add tests for …#191

Merged
timlaing merged 2 commits intomainfrom
bugfix/permission-error
Feb 21, 2026
Merged

Fix permission handling in _setup_cookie_directory and add tests for …#191
timlaing merged 2 commits intomainfrom
bugfix/permission-error

Conversation

@timlaing
Copy link
Copy Markdown
Owner

Proposed change

Bugfix for permission error, when shared between multiple users.

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New service (thank you!)
  • New feature (which adds functionality to an existing service)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests
  • Documentation or code sample

Additional information

Checklist

  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • Tests have been added to verify that the new code works.

If user exposed functionality or configuration variables are added/changed:

  • Documentation added/updated to README

@timlaing timlaing self-assigned this Feb 21, 2026
Copilot AI review requested due to automatic review settings February 21, 2026 15:07
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Refactor

    • Improved cookie-directory setup to normalise user paths, handle tilde expansion and consistently enforce secure directory permissions across defaults and custom paths.
  • Tests

    • Added comprehensive tests for cookie-directory behaviour (custom/default paths, existence checks, permissions).
    • Strengthened test isolation by mocking filesystem creation/chmod to prevent accidental writes during test runs.

Walkthrough

Updates cookie-directory setup to normalise user paths, use os.makedirs for creation, and enforce permissions via os.chmod and a temporary umask. Adds optional default cookie_directory parameter and corresponding unit tests covering existence, creation, tilde expansion and permission checks.

Changes

Cohort / File(s) Summary
Core cookie-dir logic
pyicloud/base.py
Add default cookie_directory: Optional[str] = None; normalise/expand provided paths; use os.makedirs() for top-level and user dirs; apply temporary umask(0o77) during creation; set permissions explicitly with os.chmod() (top-level 0o1777, user dir 0o700). Imports adjusted.
Unit tests — base
tests/test_base.py
Add tests for custom path (existing and non-existent), default path (None) across top-level/user-dir existence permutations, and tilde expansion. Assert calls to makedirs, chmod and returned path values.
Test fixtures
tests/conftest.py
Add autouse fixtures to patch os.makedirs and os.chmod, restricting filesystem access during tests and delegating to real calls only for test-target paths.
Test adjustments
tests/test_cmdline.py
Patch pyicloud.base.makedirs in test contexts to prevent real directory creation during command-line tests.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Session-update #47 — Modifies the same _setup_cookie_directory area; similar signature/behaviour changes.
  • (No other strong code-level matches found)

🐰 I dug a burrow, tidy and neat,
chmod and umask kept it sweet,
/tmp now welcomes every friend,
cookies cosy, permissions penned,
hop on — shared access can't be beat! 🍪

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title partially relates to the changeset, referring to the main change (permission handling in _setup_cookie_directory) but is truncated with 'and add tests for …' and lacks complete clarity.
Description check ✅ Passed The description is directly related to the changeset, explaining the bugfix for permission errors when shared between multiple users and references issue #190.
Linked Issues check ✅ Passed The PR implements the suggested fix by creating the base directory with permissive mode (1777) to allow multiple users to create subdirectories [#190].
Out of Scope Changes check ✅ Passed All changes directly support the permission handling fix and comprehensive testing. Fixture additions in conftest.py and test mocking prevent filesystem access during tests, which are appropriate supporting changes.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bugfix/permission-error

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Pylint (4.0.4)
tests/conftest.py
tests/test_base.py
tests/test_cmdline.py
  • 1 others

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses a multi-user permission issue on Linux systems where the first user creates /tmp/pyicloud with restrictive permissions, preventing other users from accessing it. The fix separates directory creation from permission setting by using mkdir() followed by explicit chmod() calls, setting the shared topdir to 0o777 permissions while maintaining 0o700 for user-specific directories.

Changes:

  • Modified _setup_cookie_directory to use separate mkdir() and chmod() calls instead of mkdir(path, mode) for more reliable permission setting
  • Set topdir (/tmp/pyicloud) permissions to 0o777 for multi-user access
  • Added comprehensive unit tests covering various directory existence scenarios

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
pyicloud/base.py Refactored directory permission handling to use explicit chmod calls; added chmod import; set topdir to 0o777 for multi-user access
tests/test_base.py Added 6 comprehensive test cases covering custom paths, default paths, directory existence scenarios, and tilde expansion

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pyicloud/base.py Outdated
if not path.exists(topdir):
mkdir(topdir, 0o777)
mkdir(topdir)
chmod(topdir, 0o777)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

The fix only sets 0o777 permissions on topdir when it's newly created (line 128-130), but doesn't fix permissions if the directory already exists with incorrect permissions (line 128 condition). If user1 initially created the directory with restrictive permissions before this fix was deployed, user2 will still encounter permission errors even after upgrading to this version. To handle the upgrade scenario where existing directories have incorrect permissions, chmod should be called on topdir unconditionally (move chmod(topdir, 0o777) outside the 'if not path.exists(topdir)' block), similar to how chmod is called unconditionally for _cookie_directory at line 134.

Suggested change
chmod(topdir, 0o777)
chmod(topdir, 0o777)

Copilot uses AI. Check for mistakes.
Comment thread pyicloud/base.py Outdated
Comment on lines +129 to +130
mkdir(topdir)
chmod(topdir, 0o777)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

There is a potential race condition between mkdir and chmod calls. If two users simultaneously create the topdir, the second user's mkdir will fail. Consider using exist_ok parameter or catching the FileExistsError exception to handle this scenario gracefully. Additionally, the chmod should be called even if mkdir fails due to concurrent creation to ensure proper permissions.

Copilot uses AI. Check for mistakes.
Comment thread pyicloud/base.py Outdated
if not path.exists(topdir):
mkdir(topdir, 0o777)
mkdir(topdir)
chmod(topdir, 0o777)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

Setting 0o777 permissions on the topdir creates a security concern as it allows all users on the system to read, write, and execute in this directory. While this is necessary for multi-user access, it could potentially allow malicious users to create symlinks or manipulate the directory structure. Consider using 0o1777 (with sticky bit) instead, which prevents users from deleting or renaming files they don't own, similar to /tmp directory permissions.

Suggested change
chmod(topdir, 0o777)
chmod(topdir, 0o1777)

Copilot uses AI. Check for mistakes.
Comment thread tests/test_base.py Outdated
assert result == "/tmp/pyicloud/testuser"
assert mock_mkdir.call_count == 1
mock_mkdir.assert_called_once_with("/tmp/pyicloud/testuser")
mock_chmod.assert_called_once_with("/tmp/pyicloud/testuser", 0o700)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

This test verifies that chmod is only called once when topdir already exists. However, this behavior is problematic for the upgrade scenario where existing topdir directories have incorrect permissions from before this fix. The test should be updated to expect chmod to be called on topdir regardless of whether it already exists, to ensure permissions are corrected. Expected behavior should be: mock_chmod.call_count == 2, with calls to both topdir and userdir.

Suggested change
mock_chmod.assert_called_once_with("/tmp/pyicloud/testuser", 0o700)
assert mock_chmod.call_count == 2
mock_chmod.assert_any_call("/tmp/pyicloud", 0o777)
mock_chmod.assert_any_call("/tmp/pyicloud/testuser", 0o700)

Copilot uses AI. Check for mistakes.
Comment thread tests/test_base.py Outdated

assert result == "/tmp/pyicloud/testuser"
mock_mkdir.assert_not_called()
mock_chmod.assert_called_once_with("/tmp/pyicloud/testuser", 0o700)
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

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

Similar to test_setup_cookie_directory_default_path_topdir_exists, this test doesn't account for the need to fix topdir permissions in the upgrade scenario. When both directories exist, the code should still call chmod on topdir to ensure it has 0o777 permissions. The test should expect chmod to be called twice: once for topdir with 0o777 and once for userdir with 0o700.

Copilot uses AI. Check for mistakes.
Comment thread pyicloud/base.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
tests/test_base.py (1)

1409-1409: Patching pyicloud.base.path.join replaces os.path.join globally within the with block

Because pyicloud.base.path is the same object as os.path, this patch affects all callers of os.path.join in the process during the block. The side_effect list has exactly 2 entries, so any unexpected third call (e.g., from pytest internals or a future change to _setup_cookie_directory) would raise StopIteration. The same applies to patch("pyicloud.base.path.exists") with side_effect lists in these tests.

♻️ Suggested alternative – avoid patching os.path directly

Prefer patching the whole path module reference so the replacement is local to pyicloud.base:

import os.path as _real_path

def _make_path_mock(join_side_effect, exists_side_effect):
    m = MagicMock(wraps=_real_path)
    m.join.side_effect = join_side_effect
    m.exists.side_effect = exists_side_effect
    return m

with patch("pyicloud.base.path", new=_make_path_mock(...)):
    ...

Or simply use a permissive side_effect callable rather than a fixed list to avoid StopIteration on unexpected calls.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_base.py` at line 1409, The test currently patches
pyicloud.base.path.join (and path.exists) which replaces os.path.join globally
and can cause StopIteration if more calls occur; update tests to patch the path
module reference instead of individual functions by replacing pyicloud.base.path
with a MagicMock that wraps the real os.path and sets join.side_effect and
exists.side_effect (or supply callable side_effects that handle any number of
calls), and apply this change where the test patches "pyicloud.base.path.join"
and "pyicloud.base.path.exists" (affecting calls from _setup_cookie_directory
and related helpers) so only pyicloud.base sees the mocked behavior and
unexpected extra calls don’t raise StopIteration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pyicloud/base.py`:
- Line 130: The directory permission set in base.py uses chmod(topdir, 0o777)
which allows other users to delete each other's cookie subdirectories; change
the mode to include the sticky bit by setting chmod(topdir, 0o1777) (i.e., use
0o1_777) so the directory remains world-writable but prevents deletion of
another user's entries—update the chmod call that references topdir accordingly.
- Around line 128-130: The current logic only calls chmod(topdir, 0o777) when
creating the directory and uses a check-then-create pattern
(path.exists(topdir); mkdir(topdir)) which leaves a TOCTOU race and won't fix
permissions for pre-existing dirs; replace the exists+mkdir sequence with a
race-safe creation using makedirs(topdir, exist_ok=True) (or os.makedirs(...,
exist_ok=True)) and then perform an unconditional chmod(topdir, 0o777) after
creation/ensurance so permissions are fixed even if the directory already
existed; add the makedirs import if needed and keep using the same topdir, chmod
and related symbols.
- Line 122: The assignment to _cookie_directory uses
path.expanduser(path.normpath(cookie_directory)) which can produce a relative
path for inputs like "~/.."; change the call order to
path.normpath(path.expanduser(cookie_directory)) in the code that sets
_cookie_directory so tilde expansion happens before normalization, and update
the test test_setup_cookie_directory_expands_tilde in tests/test_base.py (remove
or adjust the mock_normpath assumption that normpath is called first and ensure
the test asserts the expanded-and-normalized result).

In `@tests/test_base.py`:
- Around line 1486-1495: The test assumes normpath is called before expanduser;
update the mocks and assertions to reflect the corrected call order in
_setup_cookie_directory (expanduser -> normpath): set
mock_expanduser.return_value to "/home/user/.pyicloud" and set
mock_normpath.return_value (or side_effect) to "/home/user/.pyicloud", then
assert mock_expanduser.assert_called_once_with("~/.pyicloud") and
mock_normpath.assert_called_once_with("/home/user/.pyicloud") so the test no
longer depends on the old normpath-first ordering.

---

Nitpick comments:
In `@tests/test_base.py`:
- Line 1409: The test currently patches pyicloud.base.path.join (and
path.exists) which replaces os.path.join globally and can cause StopIteration if
more calls occur; update tests to patch the path module reference instead of
individual functions by replacing pyicloud.base.path with a MagicMock that wraps
the real os.path and sets join.side_effect and exists.side_effect (or supply
callable side_effects that handle any number of calls), and apply this change
where the test patches "pyicloud.base.path.join" and "pyicloud.base.path.exists"
(affecting calls from _setup_cookie_directory and related helpers) so only
pyicloud.base sees the mocked behavior and unexpected extra calls don’t raise
StopIteration.

Comment thread pyicloud/base.py Outdated
Comment thread pyicloud/base.py Outdated
Comment thread pyicloud/base.py Outdated
Comment thread tests/test_base.py Outdated
…d permission handling; add mocks in tests to prevent filesystem access.
@timlaing timlaing enabled auto-merge (squash) February 21, 2026 15:54
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Feb 21, 2026

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/test_base.py (1)

1355-1372: Consider patching pyicloud.base.umask for hermetic tests

test_setup_cookie_directory_with_custom_path, test_setup_cookie_directory_with_none_creates_default, and test_setup_cookie_directory_with_empty_string all patch pyicloud.base.makedirs but leave umask unpatched. The production code unconditionally calls umask(0o077) and umask(old_umask) in the try/finally block, so both real system calls fire during these tests. The try/finally restores the process umask safely, but the tests remain inconsistent with test_setup_cookie_directory_with_tilde_expansion, which correctly patches umask and asserts its call arguments.

Patching pyicloud.base.umask would make the three tests hermetic and allow asserting the 0o077 → restore sequence:

♻️ Example patch for `test_setup_cookie_directory_with_custom_path`
     with (
         patch("pyicloud.base.path.expanduser") as mock_expanduser,
         patch("pyicloud.base.path.normpath") as mock_normpath,
         patch("pyicloud.base.makedirs") as mock_makedirs,
+        patch("pyicloud.base.umask") as mock_umask,
     ):
         mock_normpath.return_value = "/normalized/path"
         mock_expanduser.return_value = "/expanded/path"
+        mock_umask.return_value = 0o022

         result: str = pyicloud_service._setup_cookie_directory("/custom/path")

         mock_expanduser.assert_called_once_with("/custom/path")
         mock_normpath.assert_called_once_with("/expanded/path")
         mock_makedirs.assert_called_once_with("/normalized/path", exist_ok=True)
+        mock_umask.assert_any_call(0o077)
+        mock_umask.assert_called_with(0o022)
         assert result == "/normalized/path"

Also applies to: 1375-1398, 1401-1420

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_base.py` around lines 1355 - 1372, Patch pyicloud.base.umask in
the three tests (test_setup_cookie_directory_with_custom_path,
test_setup_cookie_directory_with_none_creates_default,
test_setup_cookie_directory_with_empty_string) so the real process umask is not
changed; add a patch("pyicloud.base.umask") context manager similar to
test_setup_cookie_directory_with_tilde_expansion, set a mock return value for
the old umask, assert it's called first with 0o077 and then again to restore the
old umask, and keep existing assertions for path.expanduser, path.normpath,
makedirs and the returned result.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@tests/test_base.py`:
- Around line 1423-1444: Add an assertion to verify normpath was called with the
expanded path in test_setup_cookie_directory_with_tilde_expansion: after calling
pyicloud_service._setup_cookie_directory("~/.pyicloud") add
mock_normpath.assert_called_once_with("/home/user/.pyicloud") so the test
ensures pyicloud.base.path.normpath is invoked with the expanded user path (in
relation to the _setup_cookie_directory behavior).

---

Nitpick comments:
In `@tests/test_base.py`:
- Around line 1355-1372: Patch pyicloud.base.umask in the three tests
(test_setup_cookie_directory_with_custom_path,
test_setup_cookie_directory_with_none_creates_default,
test_setup_cookie_directory_with_empty_string) so the real process umask is not
changed; add a patch("pyicloud.base.umask") context manager similar to
test_setup_cookie_directory_with_tilde_expansion, set a mock return value for
the old umask, assert it's called first with 0o077 and then again to restore the
old umask, and keep existing assertions for path.expanduser, path.normpath,
makedirs and the returned result.

@timlaing timlaing merged commit 67cf282 into main Feb 21, 2026
22 checks passed
@timlaing timlaing deleted the bugfix/permission-error branch February 21, 2026 16:03
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.

Using pyicloud from 2 different linux users => Permission denied for 2nd user.

2 participants