jwbmisc 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {jwbmisc-0.0.4/src/jwbmisc.egg-info → jwbmisc-0.0.5}/PKG-INFO +1 -1
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/_version.py +3 -3
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/keeper.py +22 -21
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/passwd.py +2 -2
- {jwbmisc-0.0.4 → jwbmisc-0.0.5/src/jwbmisc.egg-info}/PKG-INFO +1 -1
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_keeper.py +60 -68
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/LICENSE +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/MANIFEST.in +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/Makefile +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/README.md +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/pyproject.toml +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/release.sh +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/setup.cfg +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/__init__.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/collection.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/exec.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/fs.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/interactive.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/json.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc/string.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc.egg-info/SOURCES.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc.egg-info/dependency_links.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc.egg-info/requires.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/src/jwbmisc.egg-info/top_level.txt +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/conftest.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/fzf +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/pass +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_collection.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_exec.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_fs.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_interactive.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_json.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_passwd.py +0 -0
- {jwbmisc-0.0.4 → jwbmisc-0.0.5}/tests/test_string.py +0 -0
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0, 0,
|
|
31
|
+
__version__ = version = '0.0.5'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 0, 5)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g68c0a3571'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from keepercommander import vault
|
|
2
|
+
from keepercommander import vault
|
|
3
3
|
from keepercommander import api
|
|
4
4
|
from time import sleep
|
|
5
5
|
from keepercommander.params import KeeperParams
|
|
@@ -13,8 +13,10 @@ from logging import getLogger
|
|
|
13
13
|
|
|
14
14
|
logger = getLogger(__name__)
|
|
15
15
|
|
|
16
|
+
CONFIG_FILE = Path.home() / ".config" / "jwbmisc" / "keeper.json"
|
|
16
17
|
|
|
17
|
-
|
|
18
|
+
|
|
19
|
+
class MinimalKeeperUI:
|
|
18
20
|
def __init__(self):
|
|
19
21
|
self.waiting_for_sso_data_key = False
|
|
20
22
|
|
|
@@ -65,19 +67,19 @@ class _MinimalKeeperUI:
|
|
|
65
67
|
step.resume()
|
|
66
68
|
|
|
67
69
|
|
|
68
|
-
def
|
|
69
|
-
if isinstance(record,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
def extract_record_field(record, field: str) -> str | None:
|
|
71
|
+
if not isinstance(record, vault.TypedRecord):
|
|
72
|
+
raise TypeError("only TypedRecord is supported")
|
|
73
|
+
|
|
74
|
+
value = record.get_typed_field(field) or record.get_typed_field(None, field)
|
|
73
75
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
+
if value and value.value:
|
|
77
|
+
return value.value[0] if isinstance(value.value, list) else str(value.value)
|
|
76
78
|
|
|
77
79
|
return None
|
|
78
80
|
|
|
79
81
|
|
|
80
|
-
def
|
|
82
|
+
def perform_login(params):
|
|
81
83
|
if not params.user:
|
|
82
84
|
user = os.environ.get("KEEPER_USERNAME", None)
|
|
83
85
|
if user is None:
|
|
@@ -96,29 +98,28 @@ def _perform_keeper_login(params):
|
|
|
96
98
|
params.server = server
|
|
97
99
|
|
|
98
100
|
try:
|
|
99
|
-
api.login(params, login_ui=
|
|
101
|
+
api.login(params, login_ui=MinimalKeeperUI())
|
|
100
102
|
except KeyboardInterrupt:
|
|
101
103
|
raise KeyError("\nKeeper login cancelled by user.") from None
|
|
102
104
|
except Exception as e:
|
|
103
105
|
raise KeyError(f"Keeper login failed: {e}") from e
|
|
104
106
|
|
|
105
107
|
|
|
106
|
-
def
|
|
107
|
-
|
|
108
|
-
config_file.parent.mkdir(parents=True, exist_ok=True)
|
|
108
|
+
def get_password(record_uid: str, field_path: str) -> str:
|
|
109
|
+
CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
109
110
|
|
|
110
|
-
params = KeeperParams(config_filename=str(
|
|
111
|
+
params = KeeperParams(config_filename=str(CONFIG_FILE))
|
|
111
112
|
|
|
112
|
-
if
|
|
113
|
+
if CONFIG_FILE.exists():
|
|
113
114
|
try:
|
|
114
|
-
params.config = json.loads(
|
|
115
|
+
params.config = json.loads(CONFIG_FILE.read_text())
|
|
115
116
|
loader.load_config_properties(params)
|
|
116
117
|
if not params.session_token:
|
|
117
118
|
raise ValueError("No session token")
|
|
118
119
|
except Exception:
|
|
119
|
-
|
|
120
|
+
perform_login(params)
|
|
120
121
|
else:
|
|
121
|
-
|
|
122
|
+
perform_login(params)
|
|
122
123
|
|
|
123
124
|
try:
|
|
124
125
|
api.sync_down(params)
|
|
@@ -126,11 +127,11 @@ def get_keeper_password(record_uid: str, field_path: str) -> str:
|
|
|
126
127
|
raise KeyError(f"Failed to sync Keeper vault: {e}") from e
|
|
127
128
|
|
|
128
129
|
try:
|
|
129
|
-
record =
|
|
130
|
+
record = vault.KeeperRecord.load(params, record_uid)
|
|
130
131
|
except Exception as e:
|
|
131
132
|
raise KeyError(f"Record {record_uid} not found: {e}") from e
|
|
132
133
|
|
|
133
|
-
value =
|
|
134
|
+
value = extract_record_field(record, field_path)
|
|
134
135
|
if value is None:
|
|
135
136
|
raise KeyError(f"Field '{field_path}' not found in record {record_uid}")
|
|
136
137
|
|
|
@@ -72,6 +72,6 @@ def _call_unix_pass(key, lnum=1):
|
|
|
72
72
|
|
|
73
73
|
|
|
74
74
|
def _keeper_password(record_uid: str, field_path: str) -> str:
|
|
75
|
-
from .keeper import
|
|
75
|
+
from .keeper import get_password as keeper_get_password
|
|
76
76
|
|
|
77
|
-
return
|
|
77
|
+
return keeper_get_password(record_uid, field_path)
|
|
@@ -1,69 +1,61 @@
|
|
|
1
|
+
# pyright: basic
|
|
1
2
|
import pytest
|
|
2
3
|
|
|
4
|
+
from keepercommander import vault, utils
|
|
3
5
|
from jwbmisc.keeper import (
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
MinimalKeeperUI,
|
|
7
|
+
extract_record_field,
|
|
8
|
+
perform_login,
|
|
9
|
+
get_password,
|
|
8
10
|
)
|
|
9
11
|
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
def get_typed_field(self, name):
|
|
23
|
-
return self._fields.get(name)
|
|
13
|
+
def gen_record(type_name="login"):
|
|
14
|
+
record = vault.TypedRecord()
|
|
15
|
+
record.type_name = type_name
|
|
16
|
+
record.record_uid = utils.generate_uid()
|
|
17
|
+
record.record_key = utils.generate_aes_key()
|
|
18
|
+
record.fields.append(
|
|
19
|
+
vault.TypedField.new_field(
|
|
20
|
+
field_type="password", field_label="AWS Secret Sauce", field_value=["password123"]
|
|
21
|
+
)
|
|
22
|
+
)
|
|
23
|
+
return record
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
class TestExtractKeeperField:
|
|
27
|
-
def test_typed_record_with_list_value(self
|
|
28
|
-
|
|
29
|
-
field = MockTypedField(["password123"])
|
|
30
|
-
record = MockTypedRecord(fields={"password": field})
|
|
27
|
+
def test_typed_record_with_list_value(self):
|
|
28
|
+
record = gen_record()
|
|
31
29
|
|
|
32
|
-
result =
|
|
30
|
+
result = extract_record_field(record, "password")
|
|
33
31
|
assert result == "password123"
|
|
34
32
|
|
|
35
|
-
def
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
result = _extract_keeper_field(record, "password")
|
|
41
|
-
assert result == "single_value"
|
|
42
|
-
|
|
43
|
-
def test_typed_record_field_not_found(self, mocker):
|
|
44
|
-
mocker.patch("jwbmisc.keeper.keeper_vault.TypedRecord", MockTypedRecord)
|
|
45
|
-
record = MockTypedRecord(fields={})
|
|
46
|
-
|
|
47
|
-
result = _extract_keeper_field(record, "missing")
|
|
33
|
+
def test_typed_record_field_not_found(self):
|
|
34
|
+
record = gen_record()
|
|
35
|
+
record.fields = []
|
|
36
|
+
result = extract_record_field(record, "missing")
|
|
48
37
|
assert result is None
|
|
49
38
|
|
|
50
|
-
def test_typed_record_custom_field(self
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
def test_typed_record_custom_field(self):
|
|
40
|
+
record = gen_record()
|
|
41
|
+
record.custom.append(
|
|
42
|
+
vault.TypedField.new_field(
|
|
43
|
+
field_type="passwordcustom", field_label="AWS Custom Secret Sauce", field_value=["password3"]
|
|
44
|
+
)
|
|
45
|
+
)
|
|
46
|
+
result = extract_record_field(record, "passwordcustom")
|
|
47
|
+
assert result == "password3"
|
|
48
|
+
result = extract_record_field(record, "AWS Custom Secret Sauce")
|
|
49
|
+
assert result == "password3"
|
|
58
50
|
|
|
59
51
|
def test_non_typed_record_returns_none(self):
|
|
60
|
-
|
|
61
|
-
|
|
52
|
+
with pytest.raises(TypeError):
|
|
53
|
+
extract_record_field(object(), "any_field")
|
|
62
54
|
|
|
63
55
|
|
|
64
56
|
class TestMinimalKeeperUI:
|
|
65
57
|
def test_on_password_raises(self):
|
|
66
|
-
ui =
|
|
58
|
+
ui = MinimalKeeperUI()
|
|
67
59
|
with pytest.raises(KeyError, match="Password login not supported"):
|
|
68
60
|
ui.on_password(None)
|
|
69
61
|
|
|
@@ -74,7 +66,7 @@ class TestMinimalKeeperUI:
|
|
|
74
66
|
step = mocker.MagicMock()
|
|
75
67
|
step.sso_login_url = "https://sso.example.com"
|
|
76
68
|
|
|
77
|
-
ui =
|
|
69
|
+
ui = MinimalKeeperUI()
|
|
78
70
|
ui.on_sso_redirect(step)
|
|
79
71
|
|
|
80
72
|
mock_webbrowser.open_new_tab.assert_called_once_with("https://sso.example.com")
|
|
@@ -87,7 +79,7 @@ class TestMinimalKeeperUI:
|
|
|
87
79
|
step = mocker.MagicMock()
|
|
88
80
|
step.sso_login_url = "https://sso.example.com"
|
|
89
81
|
|
|
90
|
-
ui =
|
|
82
|
+
ui = MinimalKeeperUI()
|
|
91
83
|
with pytest.raises(ValueError, match="No SSO token"):
|
|
92
84
|
ui.on_sso_redirect(step)
|
|
93
85
|
|
|
@@ -100,7 +92,7 @@ class TestMinimalKeeperUI:
|
|
|
100
92
|
step = mocker.MagicMock()
|
|
101
93
|
step.get_channels.return_value = [mock_channel]
|
|
102
94
|
|
|
103
|
-
ui =
|
|
95
|
+
ui = MinimalKeeperUI()
|
|
104
96
|
ui.on_two_factor(step)
|
|
105
97
|
|
|
106
98
|
step.send_code.assert_called_once_with("channel_123", "123456")
|
|
@@ -113,12 +105,12 @@ class TestMinimalKeeperUI:
|
|
|
113
105
|
step = mocker.MagicMock()
|
|
114
106
|
step.get_channels.return_value = [mock_channel]
|
|
115
107
|
|
|
116
|
-
ui =
|
|
108
|
+
ui = MinimalKeeperUI()
|
|
117
109
|
with pytest.raises(ValueError, match="TOTP authenticator not available"):
|
|
118
110
|
ui.on_two_factor(step)
|
|
119
111
|
|
|
120
112
|
def test_on_device_approval_does_not_raise(self, mocker):
|
|
121
|
-
ui =
|
|
113
|
+
ui = MinimalKeeperUI()
|
|
122
114
|
ui.on_device_approval(None) # Should not raise
|
|
123
115
|
|
|
124
116
|
def test_on_sso_data_key_first_call(self, mocker):
|
|
@@ -127,7 +119,7 @@ class TestMinimalKeeperUI:
|
|
|
127
119
|
|
|
128
120
|
step = mocker.MagicMock()
|
|
129
121
|
|
|
130
|
-
ui =
|
|
122
|
+
ui = MinimalKeeperUI()
|
|
131
123
|
assert ui.waiting_for_sso_data_key is False
|
|
132
124
|
|
|
133
125
|
ui.on_sso_data_key(step)
|
|
@@ -141,7 +133,7 @@ class TestMinimalKeeperUI:
|
|
|
141
133
|
mock_sleep = mocker.patch("jwbmisc.keeper.sleep")
|
|
142
134
|
step = mocker.MagicMock()
|
|
143
135
|
|
|
144
|
-
ui =
|
|
136
|
+
ui = MinimalKeeperUI()
|
|
145
137
|
ui.waiting_for_sso_data_key = True
|
|
146
138
|
|
|
147
139
|
ui.on_sso_data_key(step)
|
|
@@ -159,7 +151,7 @@ class TestPerformKeeperLogin:
|
|
|
159
151
|
params = mocker.MagicMock()
|
|
160
152
|
params.user = None
|
|
161
153
|
|
|
162
|
-
|
|
154
|
+
perform_login(params)
|
|
163
155
|
|
|
164
156
|
assert params.user == "user@example.com"
|
|
165
157
|
assert params.server == "keepersecurity.com"
|
|
@@ -173,7 +165,7 @@ class TestPerformKeeperLogin:
|
|
|
173
165
|
params = mocker.MagicMock()
|
|
174
166
|
params.user = None
|
|
175
167
|
|
|
176
|
-
|
|
168
|
+
perform_login(params)
|
|
177
169
|
|
|
178
170
|
assert params.user == "user@example.com"
|
|
179
171
|
assert params.server == "keepersecurity.com"
|
|
@@ -188,7 +180,7 @@ class TestPerformKeeperLogin:
|
|
|
188
180
|
params.user = None
|
|
189
181
|
|
|
190
182
|
with pytest.raises(KeyError, match="cancelled by user"):
|
|
191
|
-
|
|
183
|
+
perform_login(params)
|
|
192
184
|
|
|
193
185
|
|
|
194
186
|
class TestGetKeeperPassword:
|
|
@@ -200,15 +192,15 @@ class TestGetKeeperPassword:
|
|
|
200
192
|
mocker.patch("jwbmisc.keeper.KeeperParams", return_value=mock_params)
|
|
201
193
|
|
|
202
194
|
mock_api = mocker.patch("jwbmisc.keeper.api")
|
|
203
|
-
mock_vault = mocker.patch("jwbmisc.keeper.
|
|
195
|
+
mock_vault = mocker.patch("jwbmisc.keeper.vault")
|
|
204
196
|
|
|
205
197
|
mock_record = mocker.MagicMock()
|
|
206
198
|
mock_vault.KeeperRecord.load.return_value = mock_record
|
|
207
199
|
|
|
208
|
-
mocker.patch("jwbmisc.keeper.
|
|
209
|
-
mocker.patch("jwbmisc.keeper.
|
|
200
|
+
mocker.patch("jwbmisc.keeper.extract_record_field", return_value="the_password")
|
|
201
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
210
202
|
|
|
211
|
-
result =
|
|
203
|
+
result = get_password("RECORD123", "password")
|
|
212
204
|
|
|
213
205
|
assert result == "the_password"
|
|
214
206
|
mock_api.sync_down.assert_called_once()
|
|
@@ -221,14 +213,14 @@ class TestGetKeeperPassword:
|
|
|
221
213
|
mocker.patch("jwbmisc.keeper.KeeperParams", return_value=mock_params)
|
|
222
214
|
|
|
223
215
|
mocker.patch("jwbmisc.keeper.api")
|
|
224
|
-
mock_vault = mocker.patch("jwbmisc.keeper.
|
|
216
|
+
mock_vault = mocker.patch("jwbmisc.keeper.vault")
|
|
225
217
|
mock_vault.KeeperRecord.load.return_value = mocker.MagicMock()
|
|
226
218
|
|
|
227
|
-
mocker.patch("jwbmisc.keeper.
|
|
228
|
-
mocker.patch("jwbmisc.keeper.
|
|
219
|
+
mocker.patch("jwbmisc.keeper.extract_record_field", return_value=None)
|
|
220
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
229
221
|
|
|
230
222
|
with pytest.raises(KeyError, match="Field.*not found"):
|
|
231
|
-
|
|
223
|
+
get_password("RECORD123", "missing_field")
|
|
232
224
|
|
|
233
225
|
def test_sync_failure_raises(self, mocker, tmp_path):
|
|
234
226
|
mocker.patch("jwbmisc.keeper.Path.home", return_value=tmp_path)
|
|
@@ -240,10 +232,10 @@ class TestGetKeeperPassword:
|
|
|
240
232
|
mock_api = mocker.patch("jwbmisc.keeper.api")
|
|
241
233
|
mock_api.sync_down.side_effect = Exception("Sync failed")
|
|
242
234
|
|
|
243
|
-
mocker.patch("jwbmisc.keeper.
|
|
235
|
+
mocker.patch("jwbmisc.keeper.perform_login")
|
|
244
236
|
|
|
245
237
|
with pytest.raises(KeyError, match="Failed to sync"):
|
|
246
|
-
|
|
238
|
+
get_password("RECORD123", "password")
|
|
247
239
|
|
|
248
240
|
# def test_keeper_url(self, mocker):
|
|
249
241
|
# mock_keeper = mocker.patch("jwbmisc.passwd._keeper_password")
|
|
@@ -259,8 +251,8 @@ class TestGetKeeperPassword:
|
|
|
259
251
|
|
|
260
252
|
|
|
261
253
|
# class TestKeeperPassword:
|
|
262
|
-
# def
|
|
263
|
-
# mock_get = mocker.patch("jwbmisc.keeper.
|
|
254
|
+
# def test_delegates_to_get_password(self, mocker):
|
|
255
|
+
# mock_get = mocker.patch("jwbmisc.keeper.get_password")
|
|
264
256
|
# mock_get.return_value = "keeper_secret"
|
|
265
257
|
|
|
266
258
|
# result = _keeper_password("RECORD_UID", "field/path")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|