docketeer-1password 0.0.10__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.
- docketeer_1password-0.0.10/.gitignore +11 -0
- docketeer_1password-0.0.10/PKG-INFO +38 -0
- docketeer_1password-0.0.10/README.md +20 -0
- docketeer_1password-0.0.10/pyproject.toml +57 -0
- docketeer_1password-0.0.10/src/docketeer_1password/__init__.py +10 -0
- docketeer_1password-0.0.10/src/docketeer_1password/vault.py +121 -0
- docketeer_1password-0.0.10/tests/__init__.py +0 -0
- docketeer_1password-0.0.10/tests/test_init.py +14 -0
- docketeer_1password-0.0.10/tests/test_vault.py +305 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docketeer-1password
|
|
3
|
+
Version: 0.0.10
|
|
4
|
+
Summary: 1Password vault plugin for Docketeer
|
|
5
|
+
Project-URL: Homepage, https://github.com/chrisguidry/docketeer
|
|
6
|
+
Project-URL: Repository, https://github.com/chrisguidry/docketeer
|
|
7
|
+
Project-URL: Issues, https://github.com/chrisguidry/docketeer/issues
|
|
8
|
+
Author-email: Chris Guidry <guid@omg.lol>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
14
|
+
Classifier: Topic :: Security
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Requires-Dist: docketeer
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# docketeer-1password
|
|
20
|
+
|
|
21
|
+
1Password vault plugin for [Docketeer](https://github.com/chrisguidry/docketeer).
|
|
22
|
+
|
|
23
|
+
Provides secrets management backed by 1Password via the `op` CLI and service
|
|
24
|
+
account tokens. Secret names use `vault/item/field` paths (e.g.
|
|
25
|
+
`Agent/db-cred/password`).
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install docketeer-1password
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Configuration
|
|
34
|
+
|
|
35
|
+
Set `DOCKETEER_OP_SERVICE_ACCOUNT_TOKEN` in the environment to authenticate
|
|
36
|
+
with 1Password. The plugin translates this to `OP_SERVICE_ACCOUNT_TOKEN` when
|
|
37
|
+
running `op` commands. The service account grants access to one or more
|
|
38
|
+
1Password vaults.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# docketeer-1password
|
|
2
|
+
|
|
3
|
+
1Password vault plugin for [Docketeer](https://github.com/chrisguidry/docketeer).
|
|
4
|
+
|
|
5
|
+
Provides secrets management backed by 1Password via the `op` CLI and service
|
|
6
|
+
account tokens. Secret names use `vault/item/field` paths (e.g.
|
|
7
|
+
`Agent/db-cred/password`).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install docketeer-1password
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Configuration
|
|
16
|
+
|
|
17
|
+
Set `DOCKETEER_OP_SERVICE_ACCOUNT_TOKEN` in the environment to authenticate
|
|
18
|
+
with 1Password. The plugin translates this to `OP_SERVICE_ACCOUNT_TOKEN` when
|
|
19
|
+
running `op` commands. The service account grants access to one or more
|
|
20
|
+
1Password vaults.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docketeer-1password"
|
|
3
|
+
dynamic = ["version"]
|
|
4
|
+
description = "1Password vault plugin for Docketeer"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Chris Guidry", email = "guid@omg.lol" }
|
|
8
|
+
]
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Programming Language :: Python :: 3",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Topic :: Security",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"docketeer",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/chrisguidry/docketeer"
|
|
24
|
+
Repository = "https://github.com/chrisguidry/docketeer"
|
|
25
|
+
Issues = "https://github.com/chrisguidry/docketeer/issues"
|
|
26
|
+
|
|
27
|
+
[project.entry-points."docketeer.vault"]
|
|
28
|
+
onepassword = "docketeer_1password"
|
|
29
|
+
|
|
30
|
+
[build-system]
|
|
31
|
+
requires = ["hatchling", "hatch-vcs"]
|
|
32
|
+
build-backend = "hatchling.build"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.version]
|
|
35
|
+
source = "vcs"
|
|
36
|
+
raw-options.root = ".."
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/docketeer_1password"]
|
|
40
|
+
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
docketeer = { workspace = true }
|
|
43
|
+
|
|
44
|
+
[tool.pytest.ini_options]
|
|
45
|
+
minversion = "9.0"
|
|
46
|
+
addopts = [
|
|
47
|
+
"--import-mode=importlib",
|
|
48
|
+
"--cov=docketeer_1password",
|
|
49
|
+
"--cov-config=../pyproject.toml",
|
|
50
|
+
"--cov-branch",
|
|
51
|
+
"--cov-report=term-missing",
|
|
52
|
+
"--cov-fail-under=100",
|
|
53
|
+
]
|
|
54
|
+
asyncio_mode = "auto"
|
|
55
|
+
filterwarnings = [
|
|
56
|
+
"error",
|
|
57
|
+
]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from docketeer import environment
|
|
2
|
+
from docketeer_1password.vault import OnePasswordVault
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def create_vault() -> OnePasswordVault:
|
|
6
|
+
token = environment.get_str("OP_SERVICE_ACCOUNT_TOKEN")
|
|
7
|
+
return OnePasswordVault(token=token)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = ["OnePasswordVault", "create_vault"]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""1Password vault implementation using the op CLI."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
from docketeer.vault import SecretReference, Vault
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_name(name: str) -> tuple[str, str, str]:
|
|
11
|
+
"""Split a 'vault/item/field' path into (vault, item, field)."""
|
|
12
|
+
parts = name.split("/", 2)
|
|
13
|
+
if len(parts) != 3:
|
|
14
|
+
raise ValueError(f"Secret name must be 'vault/item/field', got: {name!r}")
|
|
15
|
+
return parts[0], parts[1], parts[2]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OnePasswordVault(Vault):
|
|
19
|
+
"""Vault backed by 1Password via the op CLI.
|
|
20
|
+
|
|
21
|
+
Auth uses DOCKETEER_OP_SERVICE_ACCOUNT_TOKEN, which gets translated to
|
|
22
|
+
OP_SERVICE_ACCOUNT_TOKEN in the subprocess environment so the op CLI
|
|
23
|
+
picks it up.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, token: str) -> None:
|
|
27
|
+
self._token = token
|
|
28
|
+
|
|
29
|
+
async def _run_op(self, *args: str) -> str:
|
|
30
|
+
"""Run an op CLI command and return stdout."""
|
|
31
|
+
env = {**os.environ, "OP_SERVICE_ACCOUNT_TOKEN": self._token}
|
|
32
|
+
proc = await asyncio.create_subprocess_exec(
|
|
33
|
+
"op",
|
|
34
|
+
*args,
|
|
35
|
+
env=env,
|
|
36
|
+
stdout=asyncio.subprocess.PIPE,
|
|
37
|
+
stderr=asyncio.subprocess.PIPE,
|
|
38
|
+
)
|
|
39
|
+
stdout, stderr = await proc.communicate()
|
|
40
|
+
if proc.returncode != 0:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"op {' '.join(args[:2])} failed: {stderr.decode(errors='replace').strip()}"
|
|
43
|
+
)
|
|
44
|
+
return stdout.decode(errors="replace").strip()
|
|
45
|
+
|
|
46
|
+
async def list(self) -> list[SecretReference]:
|
|
47
|
+
vaults_raw = await self._run_op("vault", "list", "--format", "json")
|
|
48
|
+
vaults = json.loads(vaults_raw)
|
|
49
|
+
if not vaults:
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
refs: list[SecretReference] = []
|
|
53
|
+
for v in vaults:
|
|
54
|
+
vault_name = v["name"]
|
|
55
|
+
items_raw = await self._run_op(
|
|
56
|
+
"item", "list", "--vault", v["id"], "--format", "json"
|
|
57
|
+
)
|
|
58
|
+
items = json.loads(items_raw)
|
|
59
|
+
for item in items:
|
|
60
|
+
detail_raw = await self._run_op(
|
|
61
|
+
"item", "get", item["id"], "--vault", v["id"], "--format", "json"
|
|
62
|
+
)
|
|
63
|
+
detail = json.loads(detail_raw)
|
|
64
|
+
for field in detail.get("fields", []):
|
|
65
|
+
refs.append(
|
|
66
|
+
SecretReference(
|
|
67
|
+
name=f"{vault_name}/{item['title']}/{field['label']}"
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
return refs
|
|
71
|
+
|
|
72
|
+
async def resolve(self, name: str) -> str:
|
|
73
|
+
vault, item, field = _parse_name(name)
|
|
74
|
+
return await self._run_op(
|
|
75
|
+
"item",
|
|
76
|
+
"get",
|
|
77
|
+
item,
|
|
78
|
+
"--vault",
|
|
79
|
+
vault,
|
|
80
|
+
"--fields",
|
|
81
|
+
field,
|
|
82
|
+
"--reveal",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
async def store(self, name: str, value: str) -> None:
|
|
86
|
+
vault, item, field = _parse_name(name)
|
|
87
|
+
await self._run_op(
|
|
88
|
+
"item",
|
|
89
|
+
"create",
|
|
90
|
+
"--category",
|
|
91
|
+
"Password",
|
|
92
|
+
"--vault",
|
|
93
|
+
vault,
|
|
94
|
+
"--title",
|
|
95
|
+
item,
|
|
96
|
+
f"{field}={value}",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
async def generate(self, name: str, length: int = 32) -> None:
|
|
100
|
+
vault, item, field = _parse_name(name)
|
|
101
|
+
await self._run_op(
|
|
102
|
+
"item",
|
|
103
|
+
"create",
|
|
104
|
+
"--category",
|
|
105
|
+
"Password",
|
|
106
|
+
"--vault",
|
|
107
|
+
vault,
|
|
108
|
+
"--title",
|
|
109
|
+
item,
|
|
110
|
+
f"--generate-password={length},letters,digits,symbols",
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def delete(self, name: str) -> None:
|
|
114
|
+
vault, item, field = _parse_name(name)
|
|
115
|
+
await self._run_op(
|
|
116
|
+
"item",
|
|
117
|
+
"delete",
|
|
118
|
+
item,
|
|
119
|
+
"--vault",
|
|
120
|
+
vault,
|
|
121
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Tests for the package entry point."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from docketeer_1password import create_vault
|
|
7
|
+
from docketeer_1password.vault import OnePasswordVault
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_create_vault_reads_docketeer_env_var():
|
|
11
|
+
with patch.dict(os.environ, {"DOCKETEER_OP_SERVICE_ACCOUNT_TOKEN": "sa-tok-123"}):
|
|
12
|
+
vault = create_vault()
|
|
13
|
+
assert isinstance(vault, OnePasswordVault)
|
|
14
|
+
assert vault._token == "sa-tok-123"
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Tests for the 1Password vault implementation."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import AsyncMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from docketeer_1password.vault import OnePasswordVault
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@pytest.fixture()
|
|
12
|
+
def vault() -> OnePasswordVault:
|
|
13
|
+
return OnePasswordVault(token="test-sa-token")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _mock_op(stdout: str = "", returncode: int = 0) -> AsyncMock:
|
|
17
|
+
"""Create a mock for asyncio.create_subprocess_exec."""
|
|
18
|
+
proc = AsyncMock()
|
|
19
|
+
proc.communicate.return_value = (stdout.encode(), b"")
|
|
20
|
+
proc.returncode = returncode
|
|
21
|
+
return proc
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --- env ---
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def test_op_receives_service_account_token(vault: OnePasswordVault):
|
|
28
|
+
vaults_json = json.dumps([])
|
|
29
|
+
|
|
30
|
+
async def fake_exec(*args: object, **kwargs: object) -> AsyncMock:
|
|
31
|
+
env: dict[str, str] = kwargs.get("env", {}) # type: ignore[assignment]
|
|
32
|
+
assert env["OP_SERVICE_ACCOUNT_TOKEN"] == "test-sa-token"
|
|
33
|
+
assert "PATH" in env
|
|
34
|
+
return _mock_op(vaults_json)
|
|
35
|
+
|
|
36
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
37
|
+
await vault.list()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# --- list ---
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _item_detail(fields: list[dict[str, str]]) -> str:
|
|
44
|
+
"""Build a JSON item detail response with the given fields."""
|
|
45
|
+
return json.dumps({"fields": fields})
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def test_list_secrets(vault: OnePasswordVault):
|
|
49
|
+
vaults_json = json.dumps([{"id": "abc", "name": "Agent"}])
|
|
50
|
+
items_json = json.dumps(
|
|
51
|
+
[
|
|
52
|
+
{"id": "item1", "title": "api-key"},
|
|
53
|
+
{"id": "item2", "title": "db-cred"},
|
|
54
|
+
]
|
|
55
|
+
)
|
|
56
|
+
detail1 = _item_detail([{"label": "password", "id": "pw"}])
|
|
57
|
+
detail2 = _item_detail(
|
|
58
|
+
[
|
|
59
|
+
{"label": "username", "id": "un"},
|
|
60
|
+
{"label": "password", "id": "pw"},
|
|
61
|
+
]
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
responses = [vaults_json, items_json, detail1, detail2]
|
|
65
|
+
call_count = 0
|
|
66
|
+
|
|
67
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
68
|
+
nonlocal call_count
|
|
69
|
+
call_count += 1
|
|
70
|
+
return _mock_op(responses[call_count - 1])
|
|
71
|
+
|
|
72
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
73
|
+
refs = await vault.list()
|
|
74
|
+
|
|
75
|
+
names = [r.name for r in refs]
|
|
76
|
+
assert "Agent/api-key/password" in names
|
|
77
|
+
assert "Agent/db-cred/username" in names
|
|
78
|
+
assert "Agent/db-cred/password" in names
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
async def test_list_multiple_vaults(vault: OnePasswordVault):
|
|
82
|
+
vaults_json = json.dumps(
|
|
83
|
+
[
|
|
84
|
+
{"id": "v1", "name": "Vault1"},
|
|
85
|
+
{"id": "v2", "name": "Vault2"},
|
|
86
|
+
]
|
|
87
|
+
)
|
|
88
|
+
items_v1 = json.dumps([{"id": "i1", "title": "secret-a"}])
|
|
89
|
+
items_v2 = json.dumps([{"id": "i2", "title": "secret-b"}])
|
|
90
|
+
detail_a = _item_detail([{"label": "password", "id": "pw"}])
|
|
91
|
+
detail_b = _item_detail([{"label": "token", "id": "tk"}])
|
|
92
|
+
|
|
93
|
+
responses = [vaults_json, items_v1, detail_a, items_v2, detail_b]
|
|
94
|
+
call_count = 0
|
|
95
|
+
|
|
96
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
97
|
+
nonlocal call_count
|
|
98
|
+
call_count += 1
|
|
99
|
+
return _mock_op(responses[call_count - 1])
|
|
100
|
+
|
|
101
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
102
|
+
refs = await vault.list()
|
|
103
|
+
|
|
104
|
+
names = {r.name for r in refs}
|
|
105
|
+
assert names == {"Vault1/secret-a/password", "Vault2/secret-b/token"}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
async def test_list_empty(vault: OnePasswordVault):
|
|
109
|
+
vaults_json = json.dumps([{"id": "v1", "name": "Agent"}])
|
|
110
|
+
items_json = json.dumps([])
|
|
111
|
+
|
|
112
|
+
responses = [vaults_json, items_json]
|
|
113
|
+
call_count = 0
|
|
114
|
+
|
|
115
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
116
|
+
nonlocal call_count
|
|
117
|
+
call_count += 1
|
|
118
|
+
return _mock_op(responses[call_count - 1])
|
|
119
|
+
|
|
120
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
121
|
+
refs = await vault.list()
|
|
122
|
+
|
|
123
|
+
assert refs == []
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
async def test_list_no_vaults(vault: OnePasswordVault):
|
|
127
|
+
vaults_json = json.dumps([])
|
|
128
|
+
|
|
129
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
130
|
+
return _mock_op(vaults_json)
|
|
131
|
+
|
|
132
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
133
|
+
refs = await vault.list()
|
|
134
|
+
|
|
135
|
+
assert refs == []
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# --- resolve ---
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
async def test_resolve(vault: OnePasswordVault):
|
|
142
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
143
|
+
return _mock_op("sk-abc123\n")
|
|
144
|
+
|
|
145
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
146
|
+
value = await vault.resolve("Agent/api-key/password")
|
|
147
|
+
|
|
148
|
+
assert value == "sk-abc123"
|
|
149
|
+
cmd = list(mock.call_args.args)
|
|
150
|
+
assert "--fields" in cmd
|
|
151
|
+
field_idx = cmd.index("--fields")
|
|
152
|
+
assert cmd[field_idx + 1] == "password"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
async def test_resolve_custom_field(vault: OnePasswordVault):
|
|
156
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
157
|
+
return _mock_op("admin")
|
|
158
|
+
|
|
159
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
160
|
+
value = await vault.resolve("Agent/db-cred/username")
|
|
161
|
+
|
|
162
|
+
assert value == "admin"
|
|
163
|
+
cmd = list(mock.call_args.args)
|
|
164
|
+
field_idx = cmd.index("--fields")
|
|
165
|
+
assert cmd[field_idx + 1] == "username"
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def test_resolve_bad_path(vault: OnePasswordVault):
|
|
169
|
+
with pytest.raises(ValueError, match="vault/item/field"):
|
|
170
|
+
await vault.resolve("no-slashes")
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
async def test_resolve_two_parts(vault: OnePasswordVault):
|
|
174
|
+
with pytest.raises(ValueError, match="vault/item/field"):
|
|
175
|
+
await vault.resolve("Agent/api-key")
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def test_resolve_op_failure(vault: OnePasswordVault):
|
|
179
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
180
|
+
proc = _mock_op("", returncode=1)
|
|
181
|
+
proc.communicate.return_value = (b"", b"not found")
|
|
182
|
+
return proc
|
|
183
|
+
|
|
184
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
185
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
186
|
+
await vault.resolve("Agent/missing/password")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --- store ---
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
async def test_store(vault: OnePasswordVault):
|
|
193
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
194
|
+
return _mock_op()
|
|
195
|
+
|
|
196
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
197
|
+
await vault.store("Agent/new-secret/password", "my-value")
|
|
198
|
+
|
|
199
|
+
cmd = list(mock.call_args.args)
|
|
200
|
+
assert "--vault" in cmd
|
|
201
|
+
assert "Agent" in cmd
|
|
202
|
+
assert "--title" in cmd
|
|
203
|
+
assert "new-secret" in cmd
|
|
204
|
+
assert "password=my-value" in cmd
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def test_store_custom_field(vault: OnePasswordVault):
|
|
208
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
209
|
+
return _mock_op()
|
|
210
|
+
|
|
211
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
212
|
+
await vault.store("Agent/db-cred/api_token", "tok-123")
|
|
213
|
+
|
|
214
|
+
cmd = list(mock.call_args.args)
|
|
215
|
+
assert "api_token=tok-123" in cmd
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
async def test_store_bad_path(vault: OnePasswordVault):
|
|
219
|
+
with pytest.raises(ValueError, match="vault/item/field"):
|
|
220
|
+
await vault.store("no-slash", "value")
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def test_store_op_failure(vault: OnePasswordVault):
|
|
224
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
225
|
+
proc = _mock_op("", returncode=1)
|
|
226
|
+
proc.communicate.return_value = (b"", b"permission denied")
|
|
227
|
+
return proc
|
|
228
|
+
|
|
229
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
230
|
+
with pytest.raises(RuntimeError, match="permission denied"):
|
|
231
|
+
await vault.store("Agent/secret/password", "value")
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# --- generate ---
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def test_generate(vault: OnePasswordVault):
|
|
238
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
239
|
+
return _mock_op()
|
|
240
|
+
|
|
241
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
242
|
+
await vault.generate("Agent/random-key/password", length=24)
|
|
243
|
+
|
|
244
|
+
cmd = list(mock.call_args.args)
|
|
245
|
+
assert any("24" in str(a) for a in cmd)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
async def test_generate_default_length(vault: OnePasswordVault):
|
|
249
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
250
|
+
return _mock_op()
|
|
251
|
+
|
|
252
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
253
|
+
await vault.generate("Agent/random-key/password")
|
|
254
|
+
|
|
255
|
+
cmd = list(mock.call_args.args)
|
|
256
|
+
assert any("32" in str(a) for a in cmd)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
async def test_generate_bad_path(vault: OnePasswordVault):
|
|
260
|
+
with pytest.raises(ValueError, match="vault/item/field"):
|
|
261
|
+
await vault.generate("no-slash")
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
async def test_generate_op_failure(vault: OnePasswordVault):
|
|
265
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
266
|
+
proc = _mock_op("", returncode=1)
|
|
267
|
+
proc.communicate.return_value = (b"", b"error")
|
|
268
|
+
return proc
|
|
269
|
+
|
|
270
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
271
|
+
with pytest.raises(RuntimeError, match="error"):
|
|
272
|
+
await vault.generate("Agent/secret/password")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# --- delete ---
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
async def test_delete(vault: OnePasswordVault):
|
|
279
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
280
|
+
return _mock_op()
|
|
281
|
+
|
|
282
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec) as mock:
|
|
283
|
+
await vault.delete("Agent/old-secret/password")
|
|
284
|
+
|
|
285
|
+
cmd = list(mock.call_args.args)
|
|
286
|
+
assert "delete" in cmd
|
|
287
|
+
assert "old-secret" in cmd
|
|
288
|
+
assert "--vault" in cmd
|
|
289
|
+
assert "Agent" in cmd
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def test_delete_bad_path(vault: OnePasswordVault):
|
|
293
|
+
with pytest.raises(ValueError, match="vault/item/field"):
|
|
294
|
+
await vault.delete("no-slash")
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
async def test_delete_op_failure(vault: OnePasswordVault):
|
|
298
|
+
async def fake_exec(*args: object, **_kwargs: object) -> AsyncMock:
|
|
299
|
+
proc = _mock_op("", returncode=1)
|
|
300
|
+
proc.communicate.return_value = (b"", b"not found")
|
|
301
|
+
return proc
|
|
302
|
+
|
|
303
|
+
with patch("asyncio.create_subprocess_exec", side_effect=fake_exec):
|
|
304
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
305
|
+
await vault.delete("Agent/missing/password")
|