datasette-secrets 0.1a0__tar.gz → 0.1a2__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.
Potentially problematic release.
This version of datasette-secrets might be problematic. Click here for more details.
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/PKG-INFO +21 -4
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/README.md +20 -3
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/__init__.py +39 -18
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/templates/secrets_index.html +5 -2
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/templates/secrets_update.html +4 -2
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/PKG-INFO +21 -4
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/pyproject.toml +1 -1
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/tests/test_secrets.py +143 -4
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/LICENSE +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/hookspecs.py +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/SOURCES.txt +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/dependency_links.txt +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/entry_points.txt +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/requires.txt +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/top_level.txt +0 -0
- {datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a2
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
6
|
License: Apache-2.0
|
|
@@ -105,6 +105,8 @@ datasette data.db --internal internal.db \
|
|
|
105
105
|
|
|
106
106
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
107
107
|
|
|
108
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
109
|
+
|
|
108
110
|
## For plugin authors
|
|
109
111
|
|
|
110
112
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -121,11 +123,24 @@ from datasette_secrets import Secret
|
|
|
121
123
|
def register_secrets():
|
|
122
124
|
return [
|
|
123
125
|
Secret(
|
|
124
|
-
"OPENAI_API_KEY",
|
|
125
|
-
|
|
126
|
+
name="OPENAI_API_KEY",
|
|
127
|
+
description="An OpenAI API key"
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
132
|
+
```python
|
|
133
|
+
@hookimpl
|
|
134
|
+
def register_secrets():
|
|
135
|
+
return [
|
|
136
|
+
Secret(
|
|
137
|
+
name="OPENAI_API_KEY",
|
|
138
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
139
|
+
obtain_label="Get an OpenAI API key"
|
|
126
140
|
),
|
|
127
141
|
]
|
|
128
142
|
```
|
|
143
|
+
|
|
129
144
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
130
145
|
|
|
131
146
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -135,12 +150,14 @@ To obtain the current value of the secret, use the `await get_secret()` method:
|
|
|
135
150
|
```python
|
|
136
151
|
from datasette_secrets import get_secret
|
|
137
152
|
|
|
138
|
-
|
|
153
|
+
# Third argument is the actor_id, optional
|
|
154
|
+
secret = await get_secret(datasette, "OPENAI_API_KEY", "root")
|
|
139
155
|
```
|
|
140
156
|
If the Datasette administrator set a `DATASETTE_SECRETS_OPENAI_API_KEY` environment variable, that will be returned.
|
|
141
157
|
|
|
142
158
|
Otherwise the encrypted value in the database table will be decrypted and returned - or `None` if there is no configured secret.
|
|
143
159
|
|
|
160
|
+
The `last_used_at` column is updated every time a secret is accessed. The `last_used_by` column will be set to the actor ID passed to `get_secret()`, or `null` if no actor ID was passed.
|
|
144
161
|
|
|
145
162
|
## Development
|
|
146
163
|
|
|
@@ -84,6 +84,8 @@ datasette data.db --internal internal.db \
|
|
|
84
84
|
|
|
85
85
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
86
86
|
|
|
87
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
88
|
+
|
|
87
89
|
## For plugin authors
|
|
88
90
|
|
|
89
91
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -100,11 +102,24 @@ from datasette_secrets import Secret
|
|
|
100
102
|
def register_secrets():
|
|
101
103
|
return [
|
|
102
104
|
Secret(
|
|
103
|
-
"OPENAI_API_KEY",
|
|
104
|
-
|
|
105
|
+
name="OPENAI_API_KEY",
|
|
106
|
+
description="An OpenAI API key"
|
|
107
|
+
),
|
|
108
|
+
]
|
|
109
|
+
```
|
|
110
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
111
|
+
```python
|
|
112
|
+
@hookimpl
|
|
113
|
+
def register_secrets():
|
|
114
|
+
return [
|
|
115
|
+
Secret(
|
|
116
|
+
name="OPENAI_API_KEY",
|
|
117
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
118
|
+
obtain_label="Get an OpenAI API key"
|
|
105
119
|
),
|
|
106
120
|
]
|
|
107
121
|
```
|
|
122
|
+
|
|
108
123
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
109
124
|
|
|
110
125
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -114,12 +129,14 @@ To obtain the current value of the secret, use the `await get_secret()` method:
|
|
|
114
129
|
```python
|
|
115
130
|
from datasette_secrets import get_secret
|
|
116
131
|
|
|
117
|
-
|
|
132
|
+
# Third argument is the actor_id, optional
|
|
133
|
+
secret = await get_secret(datasette, "OPENAI_API_KEY", "root")
|
|
118
134
|
```
|
|
119
135
|
If the Datasette administrator set a `DATASETTE_SECRETS_OPENAI_API_KEY` environment variable, that will be returned.
|
|
120
136
|
|
|
121
137
|
Otherwise the encrypted value in the database table will be decrypted and returned - or `None` if there is no configured secret.
|
|
122
138
|
|
|
139
|
+
The `last_used_at` column is updated every time a secret is accessed. The `last_used_by` column will be set to the actor ID passed to `get_secret()`, or `null` if no actor ID was passed.
|
|
123
140
|
|
|
124
141
|
## Development
|
|
125
142
|
|
|
@@ -13,7 +13,7 @@ MAX_NOTE_LENGTH = 100
|
|
|
13
13
|
pm.add_hookspecs(hookspecs)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
async def get_secret(datasette, secret_name):
|
|
16
|
+
async def get_secret(datasette, secret_name, actor_id=None):
|
|
17
17
|
secrets_by_name = {secret.name: secret for secret in await get_secrets(datasette)}
|
|
18
18
|
if secret_name not in secrets_by_name:
|
|
19
19
|
return None
|
|
@@ -24,23 +24,40 @@ async def get_secret(datasette, secret_name):
|
|
|
24
24
|
# Now look it up in the database
|
|
25
25
|
config = get_config(datasette)
|
|
26
26
|
db = get_database(datasette)
|
|
27
|
-
|
|
27
|
+
db_secret = (
|
|
28
28
|
await db.execute(
|
|
29
|
-
"select encrypted from datasette_secrets where name = ? order by version desc limit 1",
|
|
29
|
+
"select id, encrypted from datasette_secrets where name = ? order by version desc limit 1",
|
|
30
30
|
(secret_name,),
|
|
31
31
|
)
|
|
32
32
|
).first()
|
|
33
|
-
if not
|
|
33
|
+
if not db_secret:
|
|
34
34
|
return None
|
|
35
35
|
key = Fernet(config["encryption_key"].encode("utf-8"))
|
|
36
|
-
decrypted = key.decrypt(
|
|
36
|
+
decrypted = key.decrypt(db_secret["encrypted"])
|
|
37
|
+
# Update the last used timestamp and actor_id
|
|
38
|
+
params = (actor_id, db_secret["id"])
|
|
39
|
+
if not actor_id:
|
|
40
|
+
params = (db_secret["id"],)
|
|
41
|
+
await db.execute_write(
|
|
42
|
+
"""
|
|
43
|
+
update datasette_secrets
|
|
44
|
+
set last_used_at = datetime('now'),
|
|
45
|
+
last_used_by = {}
|
|
46
|
+
where id = ?
|
|
47
|
+
""".format(
|
|
48
|
+
"?" if actor_id else "null"
|
|
49
|
+
),
|
|
50
|
+
params,
|
|
51
|
+
)
|
|
37
52
|
return decrypted.decode("utf-8")
|
|
38
53
|
|
|
39
54
|
|
|
40
55
|
@dataclasses.dataclass
|
|
41
56
|
class Secret:
|
|
42
57
|
name: str
|
|
43
|
-
|
|
58
|
+
description: Optional[str] = None
|
|
59
|
+
obtain_url: Optional[str] = None
|
|
60
|
+
obtain_label: Optional[str] = None
|
|
44
61
|
|
|
45
62
|
|
|
46
63
|
SCHEMA = """
|
|
@@ -51,7 +68,6 @@ create table if not exists datasette_secrets (
|
|
|
51
68
|
version integer not null default 1,
|
|
52
69
|
encrypted blob,
|
|
53
70
|
encryption_key_name text not null,
|
|
54
|
-
redacted text,
|
|
55
71
|
created_at text,
|
|
56
72
|
created_by text,
|
|
57
73
|
updated_at text,
|
|
@@ -100,25 +116,20 @@ def register_permissions(datasette):
|
|
|
100
116
|
|
|
101
117
|
async def get_secrets(datasette):
|
|
102
118
|
secrets = []
|
|
119
|
+
seen = set()
|
|
103
120
|
for result in pm.hook.register_secrets(datasette=datasette):
|
|
104
121
|
result = await await_me_maybe(result)
|
|
105
|
-
|
|
122
|
+
for secret in result:
|
|
123
|
+
if secret.name in seen:
|
|
124
|
+
continue # Skip duplicates
|
|
125
|
+
seen.add(secret.name)
|
|
126
|
+
secrets.append(secret)
|
|
106
127
|
# if not secrets:
|
|
107
128
|
secrets.append(Secret("EXAMPLE_SECRET", "An example secret"))
|
|
108
129
|
|
|
109
130
|
return secrets
|
|
110
131
|
|
|
111
132
|
|
|
112
|
-
@hookimpl
|
|
113
|
-
def register_secrets():
|
|
114
|
-
return [
|
|
115
|
-
Secret(
|
|
116
|
-
"OPENAI_API_KEY",
|
|
117
|
-
'An OpenAI API key. Get them from <a href="https://platform.openai.com/api-keys">here</a>.',
|
|
118
|
-
),
|
|
119
|
-
]
|
|
120
|
-
|
|
121
|
-
|
|
122
133
|
@hookimpl
|
|
123
134
|
def register_commands(cli):
|
|
124
135
|
@cli.group()
|
|
@@ -169,6 +180,16 @@ async def secrets_index(datasette, request):
|
|
|
169
180
|
list(environment_secrets_names),
|
|
170
181
|
)
|
|
171
182
|
existing_secrets = {row["name"]: dict(row) for row in existing_secrets_result.rows}
|
|
183
|
+
# Try to turn updated_by into actors
|
|
184
|
+
actors = await datasette.actors_from_ids(
|
|
185
|
+
{row["updated_by"] for row in existing_secrets.values() if row["updated_by"]}
|
|
186
|
+
)
|
|
187
|
+
for secret in existing_secrets.values():
|
|
188
|
+
if secret["updated_by"]:
|
|
189
|
+
actor = actors.get(secret["updated_by"])
|
|
190
|
+
if actor:
|
|
191
|
+
display = actor.get("username") or actor.get("name") or actor.get("id")
|
|
192
|
+
secret["updated_by"] = display
|
|
172
193
|
unset_secrets = [
|
|
173
194
|
secret
|
|
174
195
|
for secret in all_secrets
|
{datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/templates/secrets_index.html
RENAMED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
<ul>
|
|
26
26
|
{% for secret in unset_secrets %}
|
|
27
27
|
<li><strong><a href="{{ urls.path("/-/secrets/") }}{{ secret.name }}">{{ secret.name }}</a></strong>
|
|
28
|
-
{% if secret.
|
|
28
|
+
{% if secret.description or secret.obtain_label %}
|
|
29
|
+
- {{ secret.description or "" }}{% if secret.description and secret.obtain_label %}, {% endif %}
|
|
30
|
+
{% if secret.obtain_label %}<a href="{{ secret.obtain_url }}">{{ secret.obtain_label }}</a>{% endif %}
|
|
31
|
+
{% endif %}</li>
|
|
29
32
|
{% endfor %}
|
|
30
33
|
</ul>
|
|
31
34
|
{% endif %}
|
|
@@ -34,7 +37,7 @@
|
|
|
34
37
|
<p style="margin-top: 2em">The following secret{% if environment_secrets|length == 1 %} is{% else %}s are{% endif %} set using environment variables:</p>
|
|
35
38
|
<ul>
|
|
36
39
|
{% for secret in environment_secrets %}
|
|
37
|
-
<li><strong>{{ secret.name }}</a></strong
|
|
40
|
+
<li><strong>{{ secret.name }}</a></strong>{% if secret.description %} - {{ secret.description }}{% endif %}<br>
|
|
38
41
|
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_{{ secret.name }}</code></span></li>
|
|
39
42
|
{% endfor %}
|
|
40
43
|
</ul>
|
{datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets/templates/secrets_update.html
RENAMED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
{% block content %}
|
|
10
10
|
<h1>{% if current_secret %}Update{% else %}Add{% endif %} secret: {{ secret_name }}</h1>
|
|
11
11
|
|
|
12
|
-
{% if secret_details
|
|
13
|
-
<p>{{ secret_details.
|
|
12
|
+
{% if secret_details.description or secret_details.obtain_label %}
|
|
13
|
+
<p>{{ secret_details.description or "" }}{% if secret_details.description and secret_details.obtain_label %}. {% endif %}
|
|
14
|
+
{% if secret_details.obtain_label %}<a href="{{ secret_details.obtain_url }}">{{ secret_details.obtain_label }}</a>{% endif %}
|
|
15
|
+
</p>
|
|
14
16
|
{% endif %}
|
|
15
17
|
|
|
16
18
|
{% if error %}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: datasette-secrets
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.1a2
|
|
4
4
|
Summary: Manage secrets such as API keys for use with other Datasette plugins
|
|
5
5
|
Author: Datasette
|
|
6
6
|
License: Apache-2.0
|
|
@@ -105,6 +105,8 @@ datasette data.db --internal internal.db \
|
|
|
105
105
|
|
|
106
106
|
users with the `manage-secrets` permission will see a new "Manage secrets" link in the Datasette navigation menu. This interface can also be accessed at `/-/secrets`.
|
|
107
107
|
|
|
108
|
+
The page with the list of secrets will show the user who last updated each secret. This will use the [actors_from_ids()](https://docs.datasette.io/en/latest/plugin_hooks.html#actors-from-ids-datasette-actor-ids) mechanism, displaying the actor's `username` if available, otherwise the `name`, otherwise the `id`.
|
|
109
|
+
|
|
108
110
|
## For plugin authors
|
|
109
111
|
|
|
110
112
|
Plugins can depend on this plugin if they want to implement secrets.
|
|
@@ -121,11 +123,24 @@ from datasette_secrets import Secret
|
|
|
121
123
|
def register_secrets():
|
|
122
124
|
return [
|
|
123
125
|
Secret(
|
|
124
|
-
"OPENAI_API_KEY",
|
|
125
|
-
|
|
126
|
+
name="OPENAI_API_KEY",
|
|
127
|
+
description="An OpenAI API key"
|
|
128
|
+
),
|
|
129
|
+
]
|
|
130
|
+
```
|
|
131
|
+
You can also provide optional `obtain_url` and `obtain_label` fields to link to a page where a user can obtain an API key:
|
|
132
|
+
```python
|
|
133
|
+
@hookimpl
|
|
134
|
+
def register_secrets():
|
|
135
|
+
return [
|
|
136
|
+
Secret(
|
|
137
|
+
name="OPENAI_API_KEY",
|
|
138
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
139
|
+
obtain_label="Get an OpenAI API key"
|
|
126
140
|
),
|
|
127
141
|
]
|
|
128
142
|
```
|
|
143
|
+
|
|
129
144
|
The hook can take an optional `datasette` argument. It can return a list or an `async def` function that, when awaited, returns a list.
|
|
130
145
|
|
|
131
146
|
The list should consist of `Secret()` instances, each with a name and an optional description. The description can contain HTML.
|
|
@@ -135,12 +150,14 @@ To obtain the current value of the secret, use the `await get_secret()` method:
|
|
|
135
150
|
```python
|
|
136
151
|
from datasette_secrets import get_secret
|
|
137
152
|
|
|
138
|
-
|
|
153
|
+
# Third argument is the actor_id, optional
|
|
154
|
+
secret = await get_secret(datasette, "OPENAI_API_KEY", "root")
|
|
139
155
|
```
|
|
140
156
|
If the Datasette administrator set a `DATASETTE_SECRETS_OPENAI_API_KEY` environment variable, that will be returned.
|
|
141
157
|
|
|
142
158
|
Otherwise the encrypted value in the database table will be decrypted and returned - or `None` if there is no configured secret.
|
|
143
159
|
|
|
160
|
+
The `last_used_at` column is updated every time a secret is accessed. The `last_used_by` column will be set to the actor ID passed to `get_secret()`, or `null` if no actor ID was passed.
|
|
144
161
|
|
|
145
162
|
## Development
|
|
146
163
|
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
from click.testing import CliRunner
|
|
2
2
|
from cryptography.fernet import Fernet
|
|
3
|
+
from datasette import hookimpl
|
|
3
4
|
from datasette.app import Datasette
|
|
4
5
|
from datasette.cli import cli
|
|
5
|
-
from
|
|
6
|
+
from datasette.plugins import pm
|
|
7
|
+
from datasette_secrets import get_secret, Secret
|
|
6
8
|
import pytest
|
|
7
9
|
from unittest.mock import ANY
|
|
8
10
|
|
|
@@ -21,6 +23,69 @@ def test_generate_command():
|
|
|
21
23
|
assert key.decrypt(key.encrypt(message)) == message
|
|
22
24
|
|
|
23
25
|
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def use_actors_plugin():
|
|
28
|
+
class ActorPlugin:
|
|
29
|
+
__name__ = "ActorPlugin"
|
|
30
|
+
|
|
31
|
+
@hookimpl
|
|
32
|
+
def actors_from_ids(self, actor_ids):
|
|
33
|
+
return {
|
|
34
|
+
id: {
|
|
35
|
+
"id": id,
|
|
36
|
+
"username": id.upper(),
|
|
37
|
+
}
|
|
38
|
+
for id in actor_ids
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
pm.register(ActorPlugin(), name="ActorPlugin")
|
|
42
|
+
yield
|
|
43
|
+
pm.unregister(name="ActorPlugin")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def register_multiple_secrets():
|
|
48
|
+
class SecretOnePlugin:
|
|
49
|
+
__name__ = "SecretOnePlugin"
|
|
50
|
+
|
|
51
|
+
@hookimpl
|
|
52
|
+
def register_secrets(self):
|
|
53
|
+
return [
|
|
54
|
+
Secret(
|
|
55
|
+
name="OPENAI_API_KEY",
|
|
56
|
+
obtain_url="https://platform.openai.com/api-keys",
|
|
57
|
+
obtain_label="Get an OpenAI API key",
|
|
58
|
+
),
|
|
59
|
+
Secret(
|
|
60
|
+
name="ANTHROPIC_API_KEY", description="A key for Anthropic's API"
|
|
61
|
+
),
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
class SecretTwoPlugin:
|
|
65
|
+
__name__ = "SecretTwoPlugin"
|
|
66
|
+
|
|
67
|
+
@hookimpl
|
|
68
|
+
def register_secrets(self):
|
|
69
|
+
return [
|
|
70
|
+
Secret(
|
|
71
|
+
name="OPENAI_API_KEY",
|
|
72
|
+
description="Just a description but should be ignored",
|
|
73
|
+
),
|
|
74
|
+
Secret(
|
|
75
|
+
name="OPENCAGE_API_KEY",
|
|
76
|
+
description="The OpenCage Geocoder",
|
|
77
|
+
obtain_url="https://opencagedata.com/dashboard",
|
|
78
|
+
obtain_label="Get an OpenCage API key",
|
|
79
|
+
),
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
pm.register(SecretTwoPlugin(), name="SecretTwoPlugin")
|
|
83
|
+
pm.register(SecretOnePlugin(), name="SecretOnePlugin")
|
|
84
|
+
yield
|
|
85
|
+
pm.unregister(name="SecretOnePlugin")
|
|
86
|
+
pm.unregister(name="SecretTwoPlugin")
|
|
87
|
+
|
|
88
|
+
|
|
24
89
|
@pytest.fixture
|
|
25
90
|
def ds():
|
|
26
91
|
return Datasette(
|
|
@@ -65,7 +130,7 @@ async def test_permissions(ds, path, verb, data, user):
|
|
|
65
130
|
|
|
66
131
|
|
|
67
132
|
@pytest.mark.asyncio
|
|
68
|
-
async def test_set_secret(ds):
|
|
133
|
+
async def test_set_secret(ds, use_actors_plugin):
|
|
69
134
|
cookies = {"ds_actor": ds.client.actor_cookie({"id": "admin"})}
|
|
70
135
|
get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies)
|
|
71
136
|
csrftoken = get_response.cookies["ds_csrftoken"]
|
|
@@ -88,7 +153,6 @@ async def test_set_secret(ds):
|
|
|
88
153
|
"version": 1,
|
|
89
154
|
"encrypted": ANY,
|
|
90
155
|
"encryption_key_name": "default",
|
|
91
|
-
"redacted": None,
|
|
92
156
|
"created_at": ANY,
|
|
93
157
|
"created_by": "admin",
|
|
94
158
|
"updated_at": ANY,
|
|
@@ -105,6 +169,13 @@ async def test_set_secret(ds):
|
|
|
105
169
|
decrypted = key.decrypt(encrypted)
|
|
106
170
|
assert decrypted == b"new-secret-value"
|
|
107
171
|
|
|
172
|
+
# Check that the listing is as expected, including showing the actor username
|
|
173
|
+
response = await ds.client.get("/-/secrets", cookies=cookies)
|
|
174
|
+
assert response.status_code == 200
|
|
175
|
+
assert "EXAMPLE_SECRET" in response.text
|
|
176
|
+
assert "new-note" in response.text
|
|
177
|
+
assert "<td>ADMIN</td>" in response.text
|
|
178
|
+
|
|
108
179
|
# Now let's edit it
|
|
109
180
|
post_response2 = await ds.client.post(
|
|
110
181
|
"/-/secrets/EXAMPLE_SECRET",
|
|
@@ -128,7 +199,6 @@ async def test_set_secret(ds):
|
|
|
128
199
|
"version": 2,
|
|
129
200
|
"encrypted": ANY,
|
|
130
201
|
"encryption_key_name": "default",
|
|
131
|
-
"redacted": None,
|
|
132
202
|
"created_at": ANY,
|
|
133
203
|
"created_by": "admin",
|
|
134
204
|
"updated_at": ANY,
|
|
@@ -147,6 +217,11 @@ async def test_get_secret(ds, monkeypatch):
|
|
|
147
217
|
get_response = await ds.client.get("/-/secrets/EXAMPLE_SECRET", cookies=cookies)
|
|
148
218
|
csrftoken = get_response.cookies["ds_csrftoken"]
|
|
149
219
|
cookies["ds_csrftoken"] = csrftoken
|
|
220
|
+
db = ds.get_internal_database()
|
|
221
|
+
# Reset state
|
|
222
|
+
await db.execute_write(
|
|
223
|
+
"update datasette_secrets set last_used_at = null, last_used_by = null"
|
|
224
|
+
)
|
|
150
225
|
post_response = await ds.client.post(
|
|
151
226
|
"/-/secrets/EXAMPLE_SECRET",
|
|
152
227
|
cookies=cookies,
|
|
@@ -158,9 +233,73 @@ async def test_get_secret(ds, monkeypatch):
|
|
|
158
233
|
)
|
|
159
234
|
assert post_response.status_code == 302
|
|
160
235
|
|
|
236
|
+
assert await get_secret(ds, "EXAMPLE_SECRET", "actor") == "manually-set-secret"
|
|
237
|
+
|
|
238
|
+
# Should have updated last_used_at and last_used_by
|
|
239
|
+
secret = (
|
|
240
|
+
await db.execute(
|
|
241
|
+
"select * from datasette_secrets where name = ? order by version desc limit 1",
|
|
242
|
+
["EXAMPLE_SECRET"],
|
|
243
|
+
)
|
|
244
|
+
).first()
|
|
245
|
+
assert secret["last_used_by"] == "actor"
|
|
246
|
+
assert secret["last_used_at"] is not None
|
|
247
|
+
|
|
248
|
+
# Calling again without actor ID should set that to null
|
|
161
249
|
assert await get_secret(ds, "EXAMPLE_SECRET") == "manually-set-secret"
|
|
250
|
+
secret2 = (
|
|
251
|
+
await db.execute(
|
|
252
|
+
"select * from datasette_secrets where name = ? order by version desc limit 1",
|
|
253
|
+
["EXAMPLE_SECRET"],
|
|
254
|
+
)
|
|
255
|
+
).first()
|
|
256
|
+
assert secret2["last_used_by"] is None
|
|
162
257
|
|
|
163
258
|
# Now over-ride with an environment variable
|
|
164
259
|
monkeypatch.setenv("DATASETTE_SECRETS_EXAMPLE_SECRET", "from env")
|
|
165
260
|
|
|
166
261
|
assert await get_secret(ds, "EXAMPLE_SECRET") == "from env"
|
|
262
|
+
|
|
263
|
+
# And check that it's shown that way on the /-/secrets page
|
|
264
|
+
response = await ds.client.get("/-/secrets", cookies=cookies)
|
|
265
|
+
assert response.status_code == 200
|
|
266
|
+
expected_html = """
|
|
267
|
+
<li><strong>EXAMPLE_SECRET</a></strong> - An example secret<br>
|
|
268
|
+
<span style="font-size: 0.8 em">Set by <code>DATASETTE_SECRETS_EXAMPLE_SECRET</code></span></li>
|
|
269
|
+
"""
|
|
270
|
+
assert remove_whitespace(expected_html) in remove_whitespace(response.text)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
@pytest.mark.asyncio
|
|
274
|
+
async def test_secret_index_page(ds, register_multiple_secrets):
|
|
275
|
+
response = await ds.client.get(
|
|
276
|
+
"/-/secrets",
|
|
277
|
+
cookies={
|
|
278
|
+
"ds_actor": ds.client.actor_cookie({"id": "admin"}),
|
|
279
|
+
},
|
|
280
|
+
)
|
|
281
|
+
assert response.status_code == 200
|
|
282
|
+
expected_html = """
|
|
283
|
+
<p style="margin-top: 2em">The following secrets have not been set:</p>
|
|
284
|
+
<ul>
|
|
285
|
+
<li><strong><a href="/-/secrets/OPENAI_API_KEY">OPENAI_API_KEY</a></strong>
|
|
286
|
+
-
|
|
287
|
+
<a href="https://platform.openai.com/api-keys">Get an OpenAI API key</a>
|
|
288
|
+
</li>
|
|
289
|
+
<li><strong><a href="/-/secrets/ANTHROPIC_API_KEY">ANTHROPIC_API_KEY</a></strong>
|
|
290
|
+
- A key for Anthropic's API
|
|
291
|
+
</li>
|
|
292
|
+
<li><strong><a href="/-/secrets/OPENCAGE_API_KEY">OPENCAGE_API_KEY</a></strong>
|
|
293
|
+
- The OpenCage Geocoder,
|
|
294
|
+
<a href="https://opencagedata.com/dashboard">Get an OpenCage API key</a>
|
|
295
|
+
</li>
|
|
296
|
+
<li><strong><a href="/-/secrets/EXAMPLE_SECRET">EXAMPLE_SECRET</a></strong>
|
|
297
|
+
- An example secret
|
|
298
|
+
</li>
|
|
299
|
+
</ul>
|
|
300
|
+
"""
|
|
301
|
+
assert remove_whitespace(expected_html) in remove_whitespace(response.text)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def remove_whitespace(s):
|
|
305
|
+
return " ".join(s.split())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
{datasette_secrets-0.1a0 → datasette_secrets-0.1a2}/datasette_secrets.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|