datasette-secrets 0.1a4__tar.gz → 0.3a0__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.
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/PKG-INFO +8 -7
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/README.md +1 -1
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/__init__.py +33 -20
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/PKG-INFO +8 -7
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/requires.txt +2 -1
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/pyproject.toml +6 -7
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/tests/test_secrets.py +27 -17
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/LICENSE +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/hookspecs.py +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/templates/secrets_index.html +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/templates/secrets_update.html +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/SOURCES.txt +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/dependency_links.txt +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/entry_points.txt +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/top_level.txt +0 -0
- {datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/setup.cfg +0 -0
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a0
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/datasette/datasette-secrets
|
|
8
8
|
Project-URL: Changelog, https://github.com/datasette/datasette-secrets/releases
|
|
9
9
|
Project-URL: Issues, https://github.com/datasette/datasette-secrets/issues
|
|
10
10
|
Project-URL: CI, https://github.com/datasette/datasette-secrets/actions
|
|
11
11
|
Classifier: Framework :: Datasette
|
|
12
|
-
|
|
13
|
-
Requires-Python: >=3.8
|
|
12
|
+
Requires-Python: >=3.10
|
|
14
13
|
Description-Content-Type: text/markdown
|
|
15
14
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: datasette>=1.
|
|
15
|
+
Requires-Dist: datasette>=1.0a21
|
|
17
16
|
Requires-Dist: cryptography
|
|
18
17
|
Provides-Extra: test
|
|
19
18
|
Requires-Dist: pytest; extra == "test"
|
|
20
19
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
20
|
+
Requires-Dist: datasette-test>=0.3.2; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# datasette-secrets
|
|
23
24
|
|
|
@@ -57,7 +58,7 @@ Configure the plugin with these these two plugin settings:
|
|
|
57
58
|
```yaml
|
|
58
59
|
plugins:
|
|
59
60
|
datasette-secrets:
|
|
60
|
-
|
|
61
|
+
encryption-key:
|
|
61
62
|
$env: DATASETTE_SECRETS_ENCRYPTION_KEY
|
|
62
63
|
database: name_of_database
|
|
63
64
|
```
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import click
|
|
2
2
|
from cryptography.fernet import Fernet
|
|
3
3
|
import dataclasses
|
|
4
|
-
from datasette import hookimpl, Forbidden,
|
|
4
|
+
from datasette import hookimpl, Forbidden, Response
|
|
5
|
+
from datasette.permissions import Action
|
|
5
6
|
from datasette.plugins import pm
|
|
6
7
|
from datasette.utils import await_me_maybe, sqlite3
|
|
7
8
|
import os
|
|
@@ -89,7 +90,7 @@ create table if not exists datasette_secrets (
|
|
|
89
90
|
def get_database(datasette):
|
|
90
91
|
plugin_config = datasette.plugin_config("datasette-secrets") or {}
|
|
91
92
|
database = plugin_config.get("database") or "_internal"
|
|
92
|
-
if database == "_internal":
|
|
93
|
+
if database == "_internal" and hasattr(datasette, "get_internal_database"):
|
|
93
94
|
return datasette.get_internal_database()
|
|
94
95
|
return datasette.get_database(database)
|
|
95
96
|
|
|
@@ -107,15 +108,11 @@ def get_config(datasette):
|
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
@hookimpl
|
|
110
|
-
def
|
|
111
|
+
def register_actions(datasette):
|
|
111
112
|
return [
|
|
112
|
-
|
|
113
|
+
Action(
|
|
113
114
|
name="manage-secrets",
|
|
114
|
-
abbr=None,
|
|
115
115
|
description="Manage Datasette secrets",
|
|
116
|
-
takes_database=False,
|
|
117
|
-
takes_resource=False,
|
|
118
|
-
default=False,
|
|
119
116
|
)
|
|
120
117
|
]
|
|
121
118
|
|
|
@@ -163,7 +160,10 @@ def startup(datasette):
|
|
|
163
160
|
|
|
164
161
|
|
|
165
162
|
async def secrets_index(datasette, request):
|
|
166
|
-
if not await datasette.
|
|
163
|
+
if not await datasette.allowed(
|
|
164
|
+
action="manage-secrets",
|
|
165
|
+
actor=request.actor,
|
|
166
|
+
):
|
|
167
167
|
raise Forbidden("Permission denied")
|
|
168
168
|
all_secrets = await get_secrets(datasette)
|
|
169
169
|
|
|
@@ -187,15 +187,22 @@ async def secrets_index(datasette, request):
|
|
|
187
187
|
)
|
|
188
188
|
existing_secrets = {row["name"]: dict(row) for row in existing_secrets_result.rows}
|
|
189
189
|
# Try to turn updated_by into actors
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
if hasattr(datasette, "actors_from_ids"):
|
|
191
|
+
actors = await datasette.actors_from_ids(
|
|
192
|
+
{
|
|
193
|
+
row["updated_by"]
|
|
194
|
+
for row in existing_secrets.values()
|
|
195
|
+
if row["updated_by"]
|
|
196
|
+
}
|
|
197
|
+
)
|
|
198
|
+
for secret in existing_secrets.values():
|
|
199
|
+
if secret["updated_by"]:
|
|
200
|
+
actor = actors.get(secret["updated_by"])
|
|
201
|
+
if actor:
|
|
202
|
+
display = (
|
|
203
|
+
actor.get("username") or actor.get("name") or actor.get("id")
|
|
204
|
+
)
|
|
205
|
+
secret["updated_by"] = display
|
|
199
206
|
unset_secrets = [
|
|
200
207
|
secret
|
|
201
208
|
for secret in all_secrets
|
|
@@ -216,7 +223,10 @@ async def secrets_index(datasette, request):
|
|
|
216
223
|
|
|
217
224
|
|
|
218
225
|
async def secrets_update(datasette, request):
|
|
219
|
-
if not await datasette.
|
|
226
|
+
if not await datasette.allowed(
|
|
227
|
+
action="manage-secrets",
|
|
228
|
+
actor=request.actor,
|
|
229
|
+
):
|
|
220
230
|
raise Forbidden("Permission denied")
|
|
221
231
|
plugin_config = get_config(datasette)
|
|
222
232
|
if not plugin_config:
|
|
@@ -340,7 +350,10 @@ def menu_links(datasette, actor):
|
|
|
340
350
|
return
|
|
341
351
|
|
|
342
352
|
async def inner():
|
|
343
|
-
if not await datasette.
|
|
353
|
+
if not await datasette.allowed(
|
|
354
|
+
action="manage-secrets",
|
|
355
|
+
actor=actor,
|
|
356
|
+
):
|
|
344
357
|
return
|
|
345
358
|
return [
|
|
346
359
|
{"href": datasette.urls.path("/-/secrets"), "label": "Manage secrets"},
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3a0
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
|
-
License: Apache-2.0
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
7
|
Project-URL: Homepage, https://github.com/datasette/datasette-secrets
|
|
8
8
|
Project-URL: Changelog, https://github.com/datasette/datasette-secrets/releases
|
|
9
9
|
Project-URL: Issues, https://github.com/datasette/datasette-secrets/issues
|
|
10
10
|
Project-URL: CI, https://github.com/datasette/datasette-secrets/actions
|
|
11
11
|
Classifier: Framework :: Datasette
|
|
12
|
-
|
|
13
|
-
Requires-Python: >=3.8
|
|
12
|
+
Requires-Python: >=3.10
|
|
14
13
|
Description-Content-Type: text/markdown
|
|
15
14
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: datasette>=1.
|
|
15
|
+
Requires-Dist: datasette>=1.0a21
|
|
17
16
|
Requires-Dist: cryptography
|
|
18
17
|
Provides-Extra: test
|
|
19
18
|
Requires-Dist: pytest; extra == "test"
|
|
20
19
|
Requires-Dist: pytest-asyncio; extra == "test"
|
|
20
|
+
Requires-Dist: datasette-test>=0.3.2; extra == "test"
|
|
21
|
+
Dynamic: license-file
|
|
21
22
|
|
|
22
23
|
# datasette-secrets
|
|
23
24
|
|
|
@@ -57,7 +58,7 @@ Configure the plugin with these these two plugin settings:
|
|
|
57
58
|
```yaml
|
|
58
59
|
plugins:
|
|
59
60
|
datasette-secrets:
|
|
60
|
-
|
|
61
|
+
encryption-key:
|
|
61
62
|
$env: DATASETTE_SECRETS_ENCRYPTION_KEY
|
|
62
63
|
database: name_of_database
|
|
63
64
|
```
|
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "datasette-secrets"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3a0"
|
|
4
4
|
description = "Manage secrets such as API keys for use with other Datasette plugins"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{name = "Datasette"}]
|
|
7
|
-
license =
|
|
7
|
+
license = "Apache-2.0"
|
|
8
8
|
classifiers=[
|
|
9
|
-
"Framework :: Datasette"
|
|
10
|
-
"License :: OSI Approved :: Apache Software License"
|
|
9
|
+
"Framework :: Datasette"
|
|
11
10
|
]
|
|
12
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.10"
|
|
13
12
|
dependencies = [
|
|
14
|
-
"datasette>=1.
|
|
13
|
+
"datasette>=1.0a21",
|
|
15
14
|
"cryptography"
|
|
16
15
|
]
|
|
17
16
|
|
|
@@ -25,7 +24,7 @@ CI = "https://github.com/datasette/datasette-secrets/actions"
|
|
|
25
24
|
secrets = "datasette_secrets"
|
|
26
25
|
|
|
27
26
|
[project.optional-dependencies]
|
|
28
|
-
test = ["pytest", "pytest-asyncio"]
|
|
27
|
+
test = ["pytest", "pytest-asyncio", "datasette-test>=0.3.2"]
|
|
29
28
|
|
|
30
29
|
[tool.pytest.ini_options]
|
|
31
30
|
asyncio_mode = "strict"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from click.testing import CliRunner
|
|
2
2
|
from cryptography.fernet import Fernet
|
|
3
3
|
from datasette import hookimpl
|
|
4
|
-
from datasette.app import Datasette
|
|
5
4
|
from datasette.cli import cli
|
|
6
5
|
from datasette.plugins import pm
|
|
6
|
+
from datasette_test import Datasette, actor_cookie
|
|
7
7
|
from datasette_secrets import get_secret, Secret, startup, get_config
|
|
8
8
|
import pytest
|
|
9
9
|
from unittest.mock import ANY
|
|
@@ -11,6 +11,13 @@ from unittest.mock import ANY
|
|
|
11
11
|
TEST_ENCRYPTION_KEY = "-LujHtwFWGaBpznrV1zduoZBmCnMOW7J0H5hmeXgAVo="
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def get_internal_database(ds):
|
|
15
|
+
if hasattr(ds, "get_internal_database"):
|
|
16
|
+
return ds.get_internal_database()
|
|
17
|
+
else:
|
|
18
|
+
return ds.get_database("_internal")
|
|
19
|
+
|
|
20
|
+
|
|
14
21
|
def test_generate_command():
|
|
15
22
|
runner = CliRunner()
|
|
16
23
|
result = runner.invoke(cli, ["secrets", "generate-encryption-key"])
|
|
@@ -89,15 +96,13 @@ def register_multiple_secrets():
|
|
|
89
96
|
@pytest.fixture
|
|
90
97
|
def ds():
|
|
91
98
|
return Datasette(
|
|
92
|
-
|
|
93
|
-
"
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
"permissions": {"manage-secrets": {"id": "admin"}},
|
|
100
|
-
}
|
|
99
|
+
plugin_config={
|
|
100
|
+
"datasette-secrets": {
|
|
101
|
+
"database": "_internal",
|
|
102
|
+
"encryption-key": TEST_ENCRYPTION_KEY,
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
permissions={"manage-secrets": {"id": "admin"}},
|
|
101
106
|
)
|
|
102
107
|
|
|
103
108
|
|
|
@@ -115,7 +120,7 @@ async def test_permissions(ds, path, verb, data, user):
|
|
|
115
120
|
kwargs = {}
|
|
116
121
|
if user:
|
|
117
122
|
kwargs["cookies"] = {
|
|
118
|
-
"ds_actor":
|
|
123
|
+
"ds_actor": actor_cookie(ds, {"id": user}),
|
|
119
124
|
}
|
|
120
125
|
if data:
|
|
121
126
|
kwargs["data"] = data
|
|
@@ -131,7 +136,7 @@ async def test_permissions(ds, path, verb, data, user):
|
|
|
131
136
|
|
|
132
137
|
@pytest.mark.asyncio
|
|
133
138
|
async def test_set_secret(ds, use_actors_plugin):
|
|
134
|
-
cookies = {"ds_actor":
|
|
139
|
+
cookies = {"ds_actor": actor_cookie(ds, {"id": "admin"})}
|
|
135
140
|
get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies)
|
|
136
141
|
csrftoken = get_response.cookies["ds_csrftoken"]
|
|
137
142
|
cookies["ds_csrftoken"] = csrftoken
|
|
@@ -142,7 +147,7 @@ async def test_set_secret(ds, use_actors_plugin):
|
|
|
142
147
|
)
|
|
143
148
|
assert post_response.status_code == 302
|
|
144
149
|
assert post_response.headers["Location"] == "/-/secrets"
|
|
145
|
-
internal_db =
|
|
150
|
+
internal_db = get_internal_database(ds)
|
|
146
151
|
secrets = await internal_db.execute("select * from datasette_secrets")
|
|
147
152
|
rows = [dict(r) for r in secrets.rows]
|
|
148
153
|
assert rows == [
|
|
@@ -174,7 +179,12 @@ async def test_set_secret(ds, use_actors_plugin):
|
|
|
174
179
|
assert response.status_code == 200
|
|
175
180
|
assert "EXAMPLE_SECRET" in response.text
|
|
176
181
|
assert "new-note" in response.text
|
|
177
|
-
|
|
182
|
+
|
|
183
|
+
if hasattr(ds, "actors_from_ids"):
|
|
184
|
+
assert "<td>ADMIN</td>" in response.text
|
|
185
|
+
else:
|
|
186
|
+
# Pre 1.0, so can't use that mechanism
|
|
187
|
+
assert "<td>admin</td>" in response.text
|
|
178
188
|
|
|
179
189
|
# Now let's edit it
|
|
180
190
|
post_response2 = await ds.client.post(
|
|
@@ -213,11 +223,11 @@ async def test_set_secret(ds, use_actors_plugin):
|
|
|
213
223
|
@pytest.mark.asyncio
|
|
214
224
|
async def test_get_secret(ds, monkeypatch):
|
|
215
225
|
# First set it manually
|
|
216
|
-
cookies = {"ds_actor":
|
|
226
|
+
cookies = {"ds_actor": actor_cookie(ds, {"id": "admin"})}
|
|
217
227
|
get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies)
|
|
218
228
|
csrftoken = get_response.cookies["ds_csrftoken"]
|
|
219
229
|
cookies["ds_csrftoken"] = csrftoken
|
|
220
|
-
db =
|
|
230
|
+
db = get_internal_database(ds)
|
|
221
231
|
# Reset state
|
|
222
232
|
await db.execute_write(
|
|
223
233
|
"update datasette_secrets set last_used_at = null, last_used_by = null"
|
|
@@ -288,7 +298,7 @@ async def test_secret_index_page(ds, register_multiple_secrets):
|
|
|
288
298
|
response = await ds.client.get(
|
|
289
299
|
"/-/secrets",
|
|
290
300
|
cookies={
|
|
291
|
-
"ds_actor":
|
|
301
|
+
"ds_actor": actor_cookie(ds, {"id": "admin"}),
|
|
292
302
|
},
|
|
293
303
|
)
|
|
294
304
|
assert response.status_code == 200
|
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/templates/secrets_index.html
RENAMED
|
File without changes
|
{datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets/templates/secrets_update.html
RENAMED
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/entry_points.txt
RENAMED
|
File without changes
|
{datasette_secrets-0.1a4 → datasette_secrets-0.3a0}/datasette_secrets.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|