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

Issue198 permission issues with privatejsonfile on windows #351

5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Fixed/improved math operator/process support for `DataCube`s in "apply" mode (non-"band math"),
allowing expressions like `10 * cube.log10()` and `~(cube == 0)`
([#123](https://github.com/Open-EO/openeo-python-client/issues/123))

- Fixed: PermissionError with PrivateJsonFile on Windows. It can now protect the file, using oschmod.
([#198](https://github.com/Open-EO/openeo-python-client/issues/198))
- Fixed: Some unit tests for configuration that were broken on Windows due to differences in paths.
([#350](https://github.com/Open-EO/openeo-python-client/issues/350))

## [0.13.0] - 2022-10-10 - "UDF UX" release

Expand Down
36 changes: 29 additions & 7 deletions openeo/rest/auth/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import json
import logging
import platform
import stat
from datetime import datetime
from pathlib import Path
Expand All @@ -18,22 +17,45 @@
from openeo.config import get_user_config_dir, get_user_data_dir
from openeo.util import rfc3339, deep_get, deep_set

# Use oschmod if it is installed.
# Otherwise we fall back on Python's chmod implementation.
# On Windows we have to use oschmod because Python chmod implementation doesn't work on Windows.
# TODO: Can we use oschmod transparantly on all platforms?
# Seems like that is the intention of oschmod, but for now we will play it safe.
try:
import oschmod
except ImportError:
oschmod = None


_PRIVATE_PERMS = stat.S_IRUSR | stat.S_IWUSR

log = logging.getLogger(__name__)


def get_file_mode(path: Path):
"""Get the file permission bits in a way that works on both *nix and Windows platforms."""
if oschmod:
return oschmod.get_mode(str(path))
return path.stat().st_mode


def set_file_mode(path: Path, mode):
"""Set the file permission bits in a way that works on both *nix and Windows platforms."""
if oschmod:
oschmod.set_mode(str(path), mode=mode)
else:
path.chmod(mode=mode)


def assert_private_file(path: Path):
"""Check that given file is only readable by user."""
mode = path.stat().st_mode
mode = get_file_mode(path)
if (mode & stat.S_IRWXG) or (mode & stat.S_IRWXO):
message = "File {p} could be readable by others: mode {a:o} (expected: {e:o}).".format(
p=path, a=mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO), e=_PRIVATE_PERMS
)
if platform.system() == 'Windows':
log.info(message)
else:
raise PermissionError(message)
raise PermissionError(message)


def utcnow_rfc3339() -> str:
Expand Down Expand Up @@ -86,7 +108,7 @@ def _write(self, data: dict):
# TODO: add file locking to avoid race conditions?
with self._path.open("w", encoding="utf8") as f:
json.dump(data, f, indent=2)
self._path.chmod(mode=_PRIVATE_PERMS)
set_file_mode(self._path, mode=_PRIVATE_PERMS)
assert_private_file(self._path)

def get(self, *keys, default=None) -> Union[dict, str, int]:
Expand Down
4 changes: 4 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
'xarray>=0.12.3',
'pandas>0.20.0',
'deprecated>=1.2.12',
'oschmod>=0.3.12; sys_platform == "win32"',
],
extras_require={
"dev": tests_require + [
Expand All @@ -55,6 +56,9 @@
"sphinx-autodoc-typehints",
"flake8",
"myst-parser",
],
"oschmod": [ # install oschmod even when platform is not Windows, e.g. for testing in CI.
"oschmod>=0.3.12"
]
},
entry_points={
Expand Down
6 changes: 3 additions & 3 deletions tests/rest/auth/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest

import openeo.rest.auth.config
from openeo.rest.auth.config import RefreshTokenStore, AuthConfig, PrivateJsonFile
from openeo.rest.auth.config import RefreshTokenStore, AuthConfig, PrivateJsonFile, get_file_mode


class TestPrivateJsonFile:
Expand All @@ -31,7 +31,7 @@ def test_permissions(self, tmp_path):
assert not private.path.exists()
private.set("foo", "bar", value=42)
assert private.path.exists()
st_mode = private.path.stat().st_mode
st_mode = get_file_mode(private.path)
assert st_mode & 0o777 == 0o600

def test_wrong_permissions(self, tmp_path):
Expand Down Expand Up @@ -151,7 +151,7 @@ def test_public_file(self, tmp_path):
def test_permissions(self, tmp_path):
r = RefreshTokenStore(path=tmp_path)
r.set_refresh_token("foo", "bar", "imd6$3cr3t")
st_mode = (tmp_path / RefreshTokenStore.DEFAULT_FILENAME).stat().st_mode
st_mode = get_file_mode(tmp_path / RefreshTokenStore.DEFAULT_FILENAME)
assert st_mode & 0o777 == 0o600

def test_get_set_refresh_token(self, tmp_path):
Expand Down
3 changes: 2 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ def test_get_config_verbose(tmp_path, caplog, capsys, verbose, force_interactive
config = get_config()
assert config.get("general.verbose") == verbose

regex = re.compile(f"Loaded.*config from.*{config_path}")
# re.escape config_path because Windows paths contain "\"
regex = re.compile(f"Loaded.*config from.*{re.escape(str(config_path))}")
assert regex.search(caplog.text)
assert bool(regex.search(capsys.readouterr().out)) == on_stdout