Skip to content

Commit 97630e8

Browse files
committed
feat!: require user to opt-in to read unsafe pins
1 parent 0263936 commit 97630e8

File tree

6 files changed

+127
-14
lines changed

6 files changed

+127
-14
lines changed

pins/boards.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(
5050
fs: IFileSystem,
5151
versioned=True,
5252
meta_factory=MetaFactory(),
53+
allow_insecure_read: "bool | None" = None,
5354
):
5455
self.board = str(board)
5556
self.fs = fs
@@ -59,6 +60,7 @@ def __init__(
5960
raise NotImplementedError()
6061

6162
self.versioned = versioned
63+
self.allow_insecure_read = allow_insecure_read
6264

6365
def pin_exists(self, name: str) -> bool:
6466
"""Determine if a pin exists.
@@ -201,7 +203,10 @@ def pin_read(self, name, version: Optional[str] = None, hash: Optional[str] = No
201203
pin_name = self.path_to_pin(name)
202204

203205
return load_data(
204-
meta, self.fs, self.construct_path([pin_name, meta.version.version])
206+
meta,
207+
self.fs,
208+
self.construct_path([pin_name, meta.version.version]),
209+
allow_insecure_read=self.allow_insecure_read,
205210
)
206211

207212
def pin_write(
@@ -603,7 +608,9 @@ def pin_download(self, name, version=None, hash=None) -> Sequence[str]:
603608
meta = self.pin_meta(name, version)
604609

605610
if isinstance(meta, MetaRaw):
606-
return load_data(meta, self.fs, None)
611+
return load_data(
612+
meta, self.fs, None, allow_insecure_read=self.allow_insecure_read
613+
)
607614

608615
raise NotImplementedError("TODO: allow download beyond MetaRaw.")
609616

pins/config.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,30 @@
22
import os
33

44
PINS_NAME = "pins-py"
5+
PINS_ENV_DATA_DIR = "PINS_DATA_DIR"
6+
PINS_ENV_CACHE_DIR = "PINS_CACHE_DIR"
7+
PINS_ENV_INSECURE_READ = "PINS_ALLOW_INSECURE_READ"
58

69

710
def get_data_dir():
8-
return os.environ.get("PINS_DATA_DIR", appdirs.user_data_dir(PINS_NAME))
11+
return os.environ.get(PINS_ENV_DATA_DIR, appdirs.user_data_dir(PINS_NAME))
912

1013

1114
def get_cache_dir():
12-
return os.environ.get("PINS_CACHE_DIR", appdirs.user_cache_dir(PINS_NAME))
15+
return os.environ.get(PINS_ENV_CACHE_DIR, appdirs.user_cache_dir(PINS_NAME))
16+
17+
18+
def get_allow_insecure_read(flag):
19+
if flag is None:
20+
env_var = os.environ.get(PINS_ENV_INSECURE_READ, "0")
21+
try:
22+
env_int = int(env_var)
23+
except ValueError:
24+
raise ValueError(
25+
f"{PINS_ENV_INSECURE_READ} must be '0' or '1', but was set to "
26+
f"{repr(env_var)}."
27+
)
28+
29+
flag = bool(env_int)
30+
31+
return flag

pins/drivers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,23 @@
22

33
from pathlib import Path
44

5+
from .config import get_allow_insecure_read, PINS_ENV_INSECURE_READ
56
from .meta import Meta
6-
7+
from .errors import PinsInsecureReadError
78

89
# TODO: move IFileSystem out of boards, to fix circular import
910
# from .boards import IFileSystem
1011

1112

12-
def load_data(meta: Meta, fs, path_to_version: "str | None" = None):
13+
UNSAFE_TYPES = frozenset(["joblib"])
14+
15+
16+
def load_data(
17+
meta: Meta,
18+
fs,
19+
path_to_version: "str | None" = None,
20+
allow_insecure_read: "bool | None" = None,
21+
):
1322
"""Return loaded data, based on meta type.
1423
Parameters
1524
----------
@@ -21,6 +30,14 @@ def load_data(meta: Meta, fs, path_to_version: "str | None" = None):
2130
A filepath used as the parent directory the data to-be-loaded lives in.
2231
"""
2332
# TODO: extandable loading with deferred importing
33+
if meta.type in UNSAFE_TYPES and not get_allow_insecure_read(allow_insecure_read):
34+
raise PinsInsecureReadError(
35+
f"Reading pin type {meta.type} is NOT secure. Set the allow_insecure_read=True "
36+
f"when creating the board, or the {PINS_ENV_INSECURE_READ}=1 environment variable.\n"
37+
"See:\n"
38+
" * https://docs.python.org/3/library/pickle.html \n"
39+
" * https://scikit-learn.org/stable/modules/model_persistence.html#security-maintainability-limitations"
40+
)
2441

2542
# Check that only a single file name was given
2643
fnames = [meta.file] if isinstance(meta.file, str) else meta.file

pins/errors.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ class PinsError(Exception):
44

55
class PinsVersionError(PinsError):
66
pass
7+
8+
9+
class PinsInsecureReadError(PinsError):
10+
pass

pins/tests/test_boards.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import fsspec
2+
import os
23
import uuid
34
import pandas as pd
45
import pytest
56

6-
from pins.tests.helpers import DEFAULT_CREATION_DATE
7-
from pins.errors import PinsError
7+
from pins.tests.helpers import DEFAULT_CREATION_DATE, rm_env
8+
from pins.errors import PinsError, PinsInsecureReadError
89
from pins.meta import MetaRaw
910

1011
from datetime import datetime, timedelta
@@ -111,15 +112,44 @@ def test_board_pin_write_rsc_index_html(board, tmp_dir2, snapshot):
111112
"obj, type_", [(df, "csv"), (df, "joblib"), ({"a": 1, "b": [2, 3]}, "joblib")]
112113
)
113114
def test_board_pin_write_type(board, obj, type_, request):
114-
meta = board.pin_write(obj, "test_pin", type=type_, title="some title")
115-
dst_obj = board.pin_read("test_pin")
115+
with rm_env("PINS_ALLOW_INSECURE_READ"):
116+
os.environ["PINS_ALLOW_INSECURE_READ"] = "1"
117+
meta = board.pin_write(obj, "test_pin", type=type_, title="some title")
118+
dst_obj = board.pin_read("test_pin")
116119

117-
assert meta.type == type_
120+
assert meta.type == type_
118121

119-
if isinstance(obj, pd.DataFrame):
120-
assert obj.equals(dst_obj)
122+
if isinstance(obj, pd.DataFrame):
123+
assert obj.equals(dst_obj)
121124

122-
obj == dst_obj
125+
obj == dst_obj
126+
127+
128+
def test_board_pin_read_insecure_fail_default(board):
129+
board.pin_write({"a": 1}, "test_pin", type="joblib", title="some title")
130+
with pytest.raises(PinsInsecureReadError) as exc_info:
131+
board.pin_read("test_pin")
132+
133+
assert "joblib" in exc_info.value.args[0]
134+
135+
136+
def test_board_pin_read_insecure_fail_board_flag(board):
137+
# board flag prioritized over env var
138+
with rm_env("PINS_ALLOW_INSECURE_READ"):
139+
os.environ["PINS_ALLOW_INSECURE_READ"] = "1"
140+
board.allow_insecure_read = False
141+
board.pin_write({"a": 1}, "test_pin", type="joblib", title="some title")
142+
with pytest.raises(PinsInsecureReadError):
143+
board.pin_read("test_pin")
144+
145+
146+
def test_board_pin_read_insecure_succeed_board_flag(board):
147+
# board flag prioritized over env var
148+
with rm_env("PINS_ALLOW_INSECURE_READ"):
149+
os.environ["PINS_ALLOW_INSECURE_READ"] = "0"
150+
board.allow_insecure_read = True
151+
board.pin_write({"a": 1}, "test_pin", type="joblib", title="some title")
152+
board.pin_read("test_pin")
123153

124154

125155
# pin_delete ==================================================================

pins/tests/test_config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import os
2+
import pytest
3+
4+
from pins.tests.helpers import rm_env
5+
from pins import config
6+
7+
8+
@pytest.fixture
9+
def env_unset():
10+
with rm_env(
11+
config.PINS_ENV_DATA_DIR,
12+
config.PINS_ENV_CACHE_DIR,
13+
config.PINS_ENV_INSECURE_READ,
14+
):
15+
yield
16+
17+
18+
def test_allow_insecure_read_no_env(env_unset):
19+
assert config.get_allow_insecure_read(True) is True
20+
assert config.get_allow_insecure_read(False) is False
21+
22+
23+
def test_allow_insecure_read_env_1(env_unset):
24+
os.environ[config.PINS_ENV_INSECURE_READ] = "1"
25+
26+
assert config.get_allow_insecure_read(True) is True
27+
assert config.get_allow_insecure_read(False) is False
28+
assert config.get_allow_insecure_read(None) is True
29+
30+
31+
def test_allow_insecure_read_env_0(env_unset):
32+
os.environ[config.PINS_ENV_INSECURE_READ] = "0"
33+
34+
assert config.get_allow_insecure_read(True) is True
35+
assert config.get_allow_insecure_read(False) is False
36+
assert config.get_allow_insecure_read(None) is False

0 commit comments

Comments
 (0)