devpi-admin 1.3.0__tar.gz → 1.4.0__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.
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/PKG-INFO +97 -33
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/README.md +96 -32
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/_version.py +3 -3
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/main.py +5 -1
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/css/style.css +285 -31
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/app.js +1706 -794
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/PKG-INFO +97 -33
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/SOURCES.txt +1 -0
- devpi_admin-1.4.0/tests/test_devpi_tokens_ui.py +527 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.github/workflows/publish.yml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.github/workflows/tests.yml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.gitignore +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/LICENSE +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/customizer.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/favicon.svg +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/index.html +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/api.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/tokens.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/dependency_links.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/entry_points.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/requires.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/top_level.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/pyproject.toml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/setup.cfg +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/__init__.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_acl_read.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_filter.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_hooks.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_package.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_pipconf.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_tokens.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_tween.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_view_helpers.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_wants_html.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: devpi-admin
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.0
|
|
4
4
|
Summary: Modern web UI plugin for devpi-server — drop-in replacement for devpi-web
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -38,16 +38,16 @@ talks to the standard devpi JSON API directly.
|
|
|
38
38
|
- Server info with version of devpi-server and all installed plugins (auto-detected)
|
|
39
39
|
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
|
|
40
40
|
- Whoosh search index queue status
|
|
41
|
-
- **Replica status** (
|
|
42
|
-
authoritative `applied_serial` vs.
|
|
43
|
-
- **in sync** - replica matches
|
|
41
|
+
- **Replica status** (primary only, authenticated users only) - per-replica cards with
|
|
42
|
+
authoritative `applied_serial` vs. primary serial. Three states:
|
|
43
|
+
- **in sync** - replica matches primary serial
|
|
44
44
|
- **lagging** - replica is behind but advancing
|
|
45
45
|
- **stuck** - replica has been polling the same serial for >=30 s; usually means a
|
|
46
46
|
server-side plugin (`devpi-admin`, `devpi-web`, ...) is missing or out of date on the replica
|
|
47
47
|
- **Topbar health indicator** - the `devpi admin` logo is coloured green / orange / red
|
|
48
48
|
on every page, refreshed every 30 s in the background:
|
|
49
49
|
- server reachable, all replicas in sync
|
|
50
|
-
- at least one replica lagging (visible to authenticated
|
|
50
|
+
- at least one replica lagging (visible to authenticated primary operators)
|
|
51
51
|
- server not responding
|
|
52
52
|
|
|
53
53
|
### Indexes
|
|
@@ -55,19 +55,13 @@ talks to the standard devpi JSON API directly.
|
|
|
55
55
|
- Warning tags for ACL edge cases:
|
|
56
56
|
- **`world-writable`** - `acl_upload` contains `:ANONYMOUS:`; supply-chain risk
|
|
57
57
|
- **`no upload`** - `acl_upload` is empty; nobody (not even owner / root) can publish
|
|
58
|
-
-
|
|
59
|
-
- **`pip.conf`** (public
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
a static pip.conf without credentials.
|
|
66
|
-
- **`.pypirc` modal** - issues an `upload`-scope token bound to the index. Returns the full
|
|
67
|
-
`.pypirc` (Copy / Download), `TWINE_*` environment variable block, a one-shot
|
|
68
|
-
`twine upload --repository-url ... -u ... -p ... dist/*` command, and the raw `user:token`
|
|
69
|
-
pair. Anonymous-upload indexes (rare; world-writable) show a static `.pypirc` without
|
|
70
|
-
credentials and a security warning.
|
|
58
|
+
- Index card kebab:
|
|
59
|
+
- **`pip.conf`** (public indexes only) — static one-click pip.conf with the index URL,
|
|
60
|
+
no token issuance needed
|
|
61
|
+
- **`Tokens`** (owner / root only) — opens the per-index unified Tokens modal with two
|
|
62
|
+
sections (Admin + Devpi), shows existing tokens for this index, lets you issue new
|
|
63
|
+
ones with the index pre-filled and locked
|
|
64
|
+
- **`Edit`** / **`Delete`** (owner / root)
|
|
71
65
|
- Create / edit / delete indexes via modal dialogs
|
|
72
66
|
- `bases` editor with drag & drop priority ordering and transitive inheritance display
|
|
73
67
|
- `acl_upload` and `acl_read` tag pickers with user selection dropdown
|
|
@@ -132,8 +126,9 @@ talks to the standard devpi JSON API directly.
|
|
|
132
126
|
users (admin delegation) but not for itself. Admin-token-authenticated requests cannot
|
|
133
127
|
issue further tokens. Issuance verifies the target user is in `acl_read` /
|
|
134
128
|
`acl_upload` of the target index.
|
|
135
|
-
- **Management rules**: list / revoke is allowed for the token owner or root.
|
|
136
|
-
token list
|
|
129
|
+
- **Management rules**: list / revoke is allowed for the token owner or root. The
|
|
130
|
+
per-index token list shows all tokens for **index owner / root**; other callers see
|
|
131
|
+
only tokens bound to themselves.
|
|
137
132
|
- **Auto-cleanup**:
|
|
138
133
|
- User delete -> all tokens for that user removed from keyfs
|
|
139
134
|
- Index delete -> all tokens bound to that index removed (USER subscriber diffs the
|
|
@@ -147,9 +142,33 @@ talks to the standard devpi JSON API directly.
|
|
|
147
142
|
|
|
148
143
|
### Users
|
|
149
144
|
- Create, edit (email, password), delete users (admin only)
|
|
150
|
-
- **Tokens manager** (kebab -> Tokens) -
|
|
151
|
-
|
|
152
|
-
|
|
145
|
+
- **Tokens manager** (kebab -> Tokens) - unified modal with one or two sections:
|
|
146
|
+
- **Admin tokens** (built-in) — per-user list with label, index, scope, expiry,
|
|
147
|
+
issuer, IP; individual revoke or "Reset all"
|
|
148
|
+
- **Devpi tokens** (only when the `devpi-tokens` plugin is installed) — list of
|
|
149
|
+
macaroon tokens with parsed restrictions (indexes, allowed permissions, projects,
|
|
150
|
+
expires, not-before); individual revoke
|
|
151
|
+
- Empty sections hide automatically (no clutter). Banner above Devpi section
|
|
152
|
+
(dismissible per user) explains the different threat model — raw secret in keyfs
|
|
153
|
+
vs. hash-only Admin storage.
|
|
154
|
+
- **Issue token** (`+ Issue new` button) — single unified modal for both backends:
|
|
155
|
+
- Token type selector at the top picks Admin vs. Devpi (Devpi default when the
|
|
156
|
+
plugin is installed). Hidden when only Admin is available.
|
|
157
|
+
- Index picker shows everything the bound user can access (owns, or appears in
|
|
158
|
+
`acl_read` / `acl_upload`). Devpi uses a multi tag picker; Admin uses a single
|
|
159
|
+
select.
|
|
160
|
+
- Admin scope dropdown adapts to the picked index: public indexes get only
|
|
161
|
+
`upload` (read tokens are useless when anyone can read), private indexes get
|
|
162
|
+
both with `read` as default.
|
|
163
|
+
- Devpi permissions are checkboxes; destructive operations (`del_*`,
|
|
164
|
+
`index_modify`, `index_delete`) are tucked behind an "Advanced" toggle with a
|
|
165
|
+
visual warning.
|
|
166
|
+
- Expiry: presets (1 hour to 1 year) plus a `Custom…` datetime option. Optional
|
|
167
|
+
Not-before for delayed activation (Devpi only).
|
|
168
|
+
- On success the modal swaps to a read-once view with the raw token, pip.conf,
|
|
169
|
+
`.pypirc`, `TWINE_*` env, and a `user:token` pair — but only the configs that
|
|
170
|
+
actually match the issued token's intent (no pip.conf for upload-only or
|
|
171
|
+
public-index tokens; no `.pypirc` for read-only tokens).
|
|
153
172
|
|
|
154
173
|
### Packages
|
|
155
174
|
- Client-side search with PEP 503 name normalization and relevance ranking
|
|
@@ -210,10 +229,10 @@ for HTML requests while `devpi-web` would still serve its own HTML on other rout
|
|
|
210
229
|
### Replicas: install on every node
|
|
211
230
|
|
|
212
231
|
`devpi-admin` registers custom keyfs keys (`+admin/tokens/...`,
|
|
213
|
-
`+admin/user-tokens/...`, `+admin/index-tokens/...`).
|
|
214
|
-
token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
215
|
-
changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
216
|
-
keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
232
|
+
`+admin/user-tokens/...`, `+admin/index-tokens/...`). The primary writes to these on
|
|
233
|
+
every token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
234
|
+
those changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
235
|
+
missing keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
217
236
|
|
|
218
237
|
The dashboard's stuck-replica detection is designed exactly for this: a `stuck`
|
|
219
238
|
state on a replica card almost always means a plugin (typically `devpi-admin` itself,
|
|
@@ -222,14 +241,14 @@ is straightforward:
|
|
|
222
241
|
|
|
223
242
|
```bash
|
|
224
243
|
# on the replica
|
|
225
|
-
~/.venv/bin/pip install --upgrade devpi-admin # match
|
|
244
|
+
~/.venv/bin/pip install --upgrade devpi-admin # match primary version
|
|
226
245
|
systemctl restart devpi
|
|
227
246
|
```
|
|
228
247
|
|
|
229
248
|
Replication resumes from the failed serial automatically - no manual keyfs surgery.
|
|
230
249
|
|
|
231
|
-
**Upgrade order:** replicas first, then
|
|
232
|
-
release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
250
|
+
**Upgrade order:** replicas first, then primary. If you upgrade the primary first and
|
|
251
|
+
that release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
233
252
|
|
|
234
253
|
See `INSTALL.md` section 11 for full step-by-step replica setup and dashboard interpretation.
|
|
235
254
|
|
|
@@ -255,6 +274,51 @@ ExecStart=/opt/pypi/venv/bin/devpi-server \
|
|
|
255
274
|
|
|
256
275
|
See `INSTALL.md` for a full systemd unit example.
|
|
257
276
|
|
|
277
|
+
### Optional plugins
|
|
278
|
+
|
|
279
|
+
#### `devpi-tokens` coexistence
|
|
280
|
+
|
|
281
|
+
`devpi-admin` plays nicely with the optional `devpi-tokens` plugin. When
|
|
282
|
+
installed, the SPA detects it (via `/+api` features list and `/+status`
|
|
283
|
+
versioninfo) and **automatically merges Devpi tokens into the same Tokens
|
|
284
|
+
modal** that lists Admin tokens — same kebab item ("Tokens"), same
|
|
285
|
+
"+ Issue new" flow, same per-index Tokens modal. The user picks the
|
|
286
|
+
backend in the Issue form via a token-type selector.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
/var/lib/pypi/venv/bin/pip install devpi-tokens
|
|
290
|
+
systemctl --user restart devpi
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
The two token systems run side by side without conflict:
|
|
294
|
+
|
|
295
|
+
| | Admin tokens | Devpi tokens |
|
|
296
|
+
|---|---|---|
|
|
297
|
+
| Plugin | `devpi-admin` (built-in) | `devpi-tokens` (optional) |
|
|
298
|
+
| Storage | SHA-256 hash in keyfs | Raw HMAC key in keyfs |
|
|
299
|
+
| Listable (incl. derived) | yes, all | initial only — derived macaroons are stateless |
|
|
300
|
+
| Audit log on lookup | yes | no |
|
|
301
|
+
| HTTP method whitelist | `read` blocks DELETE; `upload` blocks DELETE | relies on `--allowed` permission filter |
|
|
302
|
+
| Multi-index per token | no (1:1) | yes |
|
|
303
|
+
| Per-project filter | no | yes (`--projects`) |
|
|
304
|
+
| Cross-user index | no (must be in ACL) | yes (any user/index pair) |
|
|
305
|
+
| CLI compatibility | UI / API only | works with `devpi token-login` |
|
|
306
|
+
|
|
307
|
+
**Threat model note.** Macaroon HMAC verification requires the secret in
|
|
308
|
+
plaintext on the server, so `devpi-tokens` cannot hash-store; a leaked
|
|
309
|
+
backup or replica disk dump exposes working credentials. Prefer Admin
|
|
310
|
+
tokens for privileged workflows. The UI surfaces this via a (dismissible
|
|
311
|
+
per-user) security banner above the Devpi section of every Tokens modal.
|
|
312
|
+
|
|
313
|
+
`acl_read` (provided by `devpi-admin`) applies to both token systems
|
|
314
|
+
identically — devpi evaluates `pkg_read` ACL against whichever identity
|
|
315
|
+
the auth chain produced, regardless of token source.
|
|
316
|
+
|
|
317
|
+
**Testing without the plugin installed.** Append `?no-devpi-tokens` to any
|
|
318
|
+
SPA URL to make the UI behave as if the plugin weren't there (kebab item
|
|
319
|
+
disappears, type selector hides, etc.). Saves you a `pip uninstall + restart`
|
|
320
|
+
round-trip when verifying graceful degradation.
|
|
321
|
+
|
|
258
322
|
## Usage
|
|
259
323
|
|
|
260
324
|
After restart, open:
|
|
@@ -527,7 +591,7 @@ Revoke a single token.
|
|
|
527
591
|
- **200:** `{"revoked": true, "id": "abc..."}`
|
|
528
592
|
- **404:** token id not found
|
|
529
593
|
|
|
530
|
-
### Replication observability (
|
|
594
|
+
### Replication observability (primary only)
|
|
531
595
|
|
|
532
596
|
#### `GET /+admin-api/replicas`
|
|
533
597
|
Last-known poll info per replica, captured from each `GET /+changelog/{N}-` request via
|
|
@@ -538,7 +602,7 @@ applied (`start_serial - 1` from its most recent poll). Compare against `/+statu
|
|
|
538
602
|
Why this isn't `polling_replicas` from `/+status`: devpi-server overwrites
|
|
539
603
|
`xom.polling_replicas[uuid].serial` during streaming and gives a misleading "caught up"
|
|
540
604
|
reading once the response generator drains. Capturing `start_serial` at the request
|
|
541
|
-
boundary is the only stable signal
|
|
605
|
+
boundary is the only stable signal the primary alone can produce.
|
|
542
606
|
|
|
543
607
|
- **Auth:** required
|
|
544
608
|
- **200:**
|
|
@@ -558,7 +622,7 @@ boundary is the only stable signal master alone can produce.
|
|
|
558
622
|
}
|
|
559
623
|
```
|
|
560
624
|
- Entries auto-expire after 10 min of silence. Dict size capped at 256 entries
|
|
561
|
-
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust
|
|
625
|
+
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust primary memory.
|
|
562
626
|
|
|
563
627
|
## Project layout
|
|
564
628
|
|
|
@@ -14,16 +14,16 @@ talks to the standard devpi JSON API directly.
|
|
|
14
14
|
- Server info with version of devpi-server and all installed plugins (auto-detected)
|
|
15
15
|
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
|
|
16
16
|
- Whoosh search index queue status
|
|
17
|
-
- **Replica status** (
|
|
18
|
-
authoritative `applied_serial` vs.
|
|
19
|
-
- **in sync** - replica matches
|
|
17
|
+
- **Replica status** (primary only, authenticated users only) - per-replica cards with
|
|
18
|
+
authoritative `applied_serial` vs. primary serial. Three states:
|
|
19
|
+
- **in sync** - replica matches primary serial
|
|
20
20
|
- **lagging** - replica is behind but advancing
|
|
21
21
|
- **stuck** - replica has been polling the same serial for >=30 s; usually means a
|
|
22
22
|
server-side plugin (`devpi-admin`, `devpi-web`, ...) is missing or out of date on the replica
|
|
23
23
|
- **Topbar health indicator** - the `devpi admin` logo is coloured green / orange / red
|
|
24
24
|
on every page, refreshed every 30 s in the background:
|
|
25
25
|
- server reachable, all replicas in sync
|
|
26
|
-
- at least one replica lagging (visible to authenticated
|
|
26
|
+
- at least one replica lagging (visible to authenticated primary operators)
|
|
27
27
|
- server not responding
|
|
28
28
|
|
|
29
29
|
### Indexes
|
|
@@ -31,19 +31,13 @@ talks to the standard devpi JSON API directly.
|
|
|
31
31
|
- Warning tags for ACL edge cases:
|
|
32
32
|
- **`world-writable`** - `acl_upload` contains `:ANONYMOUS:`; supply-chain risk
|
|
33
33
|
- **`no upload`** - `acl_upload` is empty; nobody (not even owner / root) can publish
|
|
34
|
-
-
|
|
35
|
-
- **`pip.conf`** (public
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
a static pip.conf without credentials.
|
|
42
|
-
- **`.pypirc` modal** - issues an `upload`-scope token bound to the index. Returns the full
|
|
43
|
-
`.pypirc` (Copy / Download), `TWINE_*` environment variable block, a one-shot
|
|
44
|
-
`twine upload --repository-url ... -u ... -p ... dist/*` command, and the raw `user:token`
|
|
45
|
-
pair. Anonymous-upload indexes (rare; world-writable) show a static `.pypirc` without
|
|
46
|
-
credentials and a security warning.
|
|
34
|
+
- Index card kebab:
|
|
35
|
+
- **`pip.conf`** (public indexes only) — static one-click pip.conf with the index URL,
|
|
36
|
+
no token issuance needed
|
|
37
|
+
- **`Tokens`** (owner / root only) — opens the per-index unified Tokens modal with two
|
|
38
|
+
sections (Admin + Devpi), shows existing tokens for this index, lets you issue new
|
|
39
|
+
ones with the index pre-filled and locked
|
|
40
|
+
- **`Edit`** / **`Delete`** (owner / root)
|
|
47
41
|
- Create / edit / delete indexes via modal dialogs
|
|
48
42
|
- `bases` editor with drag & drop priority ordering and transitive inheritance display
|
|
49
43
|
- `acl_upload` and `acl_read` tag pickers with user selection dropdown
|
|
@@ -108,8 +102,9 @@ talks to the standard devpi JSON API directly.
|
|
|
108
102
|
users (admin delegation) but not for itself. Admin-token-authenticated requests cannot
|
|
109
103
|
issue further tokens. Issuance verifies the target user is in `acl_read` /
|
|
110
104
|
`acl_upload` of the target index.
|
|
111
|
-
- **Management rules**: list / revoke is allowed for the token owner or root.
|
|
112
|
-
token list
|
|
105
|
+
- **Management rules**: list / revoke is allowed for the token owner or root. The
|
|
106
|
+
per-index token list shows all tokens for **index owner / root**; other callers see
|
|
107
|
+
only tokens bound to themselves.
|
|
113
108
|
- **Auto-cleanup**:
|
|
114
109
|
- User delete -> all tokens for that user removed from keyfs
|
|
115
110
|
- Index delete -> all tokens bound to that index removed (USER subscriber diffs the
|
|
@@ -123,9 +118,33 @@ talks to the standard devpi JSON API directly.
|
|
|
123
118
|
|
|
124
119
|
### Users
|
|
125
120
|
- Create, edit (email, password), delete users (admin only)
|
|
126
|
-
- **Tokens manager** (kebab -> Tokens) -
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
- **Tokens manager** (kebab -> Tokens) - unified modal with one or two sections:
|
|
122
|
+
- **Admin tokens** (built-in) — per-user list with label, index, scope, expiry,
|
|
123
|
+
issuer, IP; individual revoke or "Reset all"
|
|
124
|
+
- **Devpi tokens** (only when the `devpi-tokens` plugin is installed) — list of
|
|
125
|
+
macaroon tokens with parsed restrictions (indexes, allowed permissions, projects,
|
|
126
|
+
expires, not-before); individual revoke
|
|
127
|
+
- Empty sections hide automatically (no clutter). Banner above Devpi section
|
|
128
|
+
(dismissible per user) explains the different threat model — raw secret in keyfs
|
|
129
|
+
vs. hash-only Admin storage.
|
|
130
|
+
- **Issue token** (`+ Issue new` button) — single unified modal for both backends:
|
|
131
|
+
- Token type selector at the top picks Admin vs. Devpi (Devpi default when the
|
|
132
|
+
plugin is installed). Hidden when only Admin is available.
|
|
133
|
+
- Index picker shows everything the bound user can access (owns, or appears in
|
|
134
|
+
`acl_read` / `acl_upload`). Devpi uses a multi tag picker; Admin uses a single
|
|
135
|
+
select.
|
|
136
|
+
- Admin scope dropdown adapts to the picked index: public indexes get only
|
|
137
|
+
`upload` (read tokens are useless when anyone can read), private indexes get
|
|
138
|
+
both with `read` as default.
|
|
139
|
+
- Devpi permissions are checkboxes; destructive operations (`del_*`,
|
|
140
|
+
`index_modify`, `index_delete`) are tucked behind an "Advanced" toggle with a
|
|
141
|
+
visual warning.
|
|
142
|
+
- Expiry: presets (1 hour to 1 year) plus a `Custom…` datetime option. Optional
|
|
143
|
+
Not-before for delayed activation (Devpi only).
|
|
144
|
+
- On success the modal swaps to a read-once view with the raw token, pip.conf,
|
|
145
|
+
`.pypirc`, `TWINE_*` env, and a `user:token` pair — but only the configs that
|
|
146
|
+
actually match the issued token's intent (no pip.conf for upload-only or
|
|
147
|
+
public-index tokens; no `.pypirc` for read-only tokens).
|
|
129
148
|
|
|
130
149
|
### Packages
|
|
131
150
|
- Client-side search with PEP 503 name normalization and relevance ranking
|
|
@@ -186,10 +205,10 @@ for HTML requests while `devpi-web` would still serve its own HTML on other rout
|
|
|
186
205
|
### Replicas: install on every node
|
|
187
206
|
|
|
188
207
|
`devpi-admin` registers custom keyfs keys (`+admin/tokens/...`,
|
|
189
|
-
`+admin/user-tokens/...`, `+admin/index-tokens/...`).
|
|
190
|
-
token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
191
|
-
changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
192
|
-
keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
208
|
+
`+admin/user-tokens/...`, `+admin/index-tokens/...`). The primary writes to these on
|
|
209
|
+
every token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
210
|
+
those changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
211
|
+
missing keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
193
212
|
|
|
194
213
|
The dashboard's stuck-replica detection is designed exactly for this: a `stuck`
|
|
195
214
|
state on a replica card almost always means a plugin (typically `devpi-admin` itself,
|
|
@@ -198,14 +217,14 @@ is straightforward:
|
|
|
198
217
|
|
|
199
218
|
```bash
|
|
200
219
|
# on the replica
|
|
201
|
-
~/.venv/bin/pip install --upgrade devpi-admin # match
|
|
220
|
+
~/.venv/bin/pip install --upgrade devpi-admin # match primary version
|
|
202
221
|
systemctl restart devpi
|
|
203
222
|
```
|
|
204
223
|
|
|
205
224
|
Replication resumes from the failed serial automatically - no manual keyfs surgery.
|
|
206
225
|
|
|
207
|
-
**Upgrade order:** replicas first, then
|
|
208
|
-
release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
226
|
+
**Upgrade order:** replicas first, then primary. If you upgrade the primary first and
|
|
227
|
+
that release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
209
228
|
|
|
210
229
|
See `INSTALL.md` section 11 for full step-by-step replica setup and dashboard interpretation.
|
|
211
230
|
|
|
@@ -231,6 +250,51 @@ ExecStart=/opt/pypi/venv/bin/devpi-server \
|
|
|
231
250
|
|
|
232
251
|
See `INSTALL.md` for a full systemd unit example.
|
|
233
252
|
|
|
253
|
+
### Optional plugins
|
|
254
|
+
|
|
255
|
+
#### `devpi-tokens` coexistence
|
|
256
|
+
|
|
257
|
+
`devpi-admin` plays nicely with the optional `devpi-tokens` plugin. When
|
|
258
|
+
installed, the SPA detects it (via `/+api` features list and `/+status`
|
|
259
|
+
versioninfo) and **automatically merges Devpi tokens into the same Tokens
|
|
260
|
+
modal** that lists Admin tokens — same kebab item ("Tokens"), same
|
|
261
|
+
"+ Issue new" flow, same per-index Tokens modal. The user picks the
|
|
262
|
+
backend in the Issue form via a token-type selector.
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
/var/lib/pypi/venv/bin/pip install devpi-tokens
|
|
266
|
+
systemctl --user restart devpi
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The two token systems run side by side without conflict:
|
|
270
|
+
|
|
271
|
+
| | Admin tokens | Devpi tokens |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| Plugin | `devpi-admin` (built-in) | `devpi-tokens` (optional) |
|
|
274
|
+
| Storage | SHA-256 hash in keyfs | Raw HMAC key in keyfs |
|
|
275
|
+
| Listable (incl. derived) | yes, all | initial only — derived macaroons are stateless |
|
|
276
|
+
| Audit log on lookup | yes | no |
|
|
277
|
+
| HTTP method whitelist | `read` blocks DELETE; `upload` blocks DELETE | relies on `--allowed` permission filter |
|
|
278
|
+
| Multi-index per token | no (1:1) | yes |
|
|
279
|
+
| Per-project filter | no | yes (`--projects`) |
|
|
280
|
+
| Cross-user index | no (must be in ACL) | yes (any user/index pair) |
|
|
281
|
+
| CLI compatibility | UI / API only | works with `devpi token-login` |
|
|
282
|
+
|
|
283
|
+
**Threat model note.** Macaroon HMAC verification requires the secret in
|
|
284
|
+
plaintext on the server, so `devpi-tokens` cannot hash-store; a leaked
|
|
285
|
+
backup or replica disk dump exposes working credentials. Prefer Admin
|
|
286
|
+
tokens for privileged workflows. The UI surfaces this via a (dismissible
|
|
287
|
+
per-user) security banner above the Devpi section of every Tokens modal.
|
|
288
|
+
|
|
289
|
+
`acl_read` (provided by `devpi-admin`) applies to both token systems
|
|
290
|
+
identically — devpi evaluates `pkg_read` ACL against whichever identity
|
|
291
|
+
the auth chain produced, regardless of token source.
|
|
292
|
+
|
|
293
|
+
**Testing without the plugin installed.** Append `?no-devpi-tokens` to any
|
|
294
|
+
SPA URL to make the UI behave as if the plugin weren't there (kebab item
|
|
295
|
+
disappears, type selector hides, etc.). Saves you a `pip uninstall + restart`
|
|
296
|
+
round-trip when verifying graceful degradation.
|
|
297
|
+
|
|
234
298
|
## Usage
|
|
235
299
|
|
|
236
300
|
After restart, open:
|
|
@@ -503,7 +567,7 @@ Revoke a single token.
|
|
|
503
567
|
- **200:** `{"revoked": true, "id": "abc..."}`
|
|
504
568
|
- **404:** token id not found
|
|
505
569
|
|
|
506
|
-
### Replication observability (
|
|
570
|
+
### Replication observability (primary only)
|
|
507
571
|
|
|
508
572
|
#### `GET /+admin-api/replicas`
|
|
509
573
|
Last-known poll info per replica, captured from each `GET /+changelog/{N}-` request via
|
|
@@ -514,7 +578,7 @@ applied (`start_serial - 1` from its most recent poll). Compare against `/+statu
|
|
|
514
578
|
Why this isn't `polling_replicas` from `/+status`: devpi-server overwrites
|
|
515
579
|
`xom.polling_replicas[uuid].serial` during streaming and gives a misleading "caught up"
|
|
516
580
|
reading once the response generator drains. Capturing `start_serial` at the request
|
|
517
|
-
boundary is the only stable signal
|
|
581
|
+
boundary is the only stable signal the primary alone can produce.
|
|
518
582
|
|
|
519
583
|
- **Auth:** required
|
|
520
584
|
- **200:**
|
|
@@ -534,7 +598,7 @@ boundary is the only stable signal master alone can produce.
|
|
|
534
598
|
}
|
|
535
599
|
```
|
|
536
600
|
- Entries auto-expire after 10 min of silence. Dict size capped at 256 entries
|
|
537
|
-
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust
|
|
601
|
+
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust primary memory.
|
|
538
602
|
|
|
539
603
|
## Project layout
|
|
540
604
|
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 0)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gc695723cf'
|
|
@@ -1345,7 +1345,11 @@ def _list_index_tokens_view(request):
|
|
|
1345
1345
|
# Hide existence from anyone without read access.
|
|
1346
1346
|
raise HTTPNotFound(json_body={"error": "index not found"})
|
|
1347
1347
|
items = tokens.list_for_index(xom, idx_user, idx_name)
|
|
1348
|
-
|
|
1348
|
+
# Index owner / root see every token bound to this index — they're
|
|
1349
|
+
# the people responsible for revoke decisions and need full audit
|
|
1350
|
+
# visibility. Non-owners see only their own tokens (the bound user
|
|
1351
|
+
# they themselves authenticate as), matching the per-user listing.
|
|
1352
|
+
if auth_user != "root" and auth_user != idx_user:
|
|
1349
1353
|
items = [(tid, meta) for tid, meta in items
|
|
1350
1354
|
if meta.get("user") == auth_user]
|
|
1351
1355
|
result = [_format_token_record(tid, meta) for tid, meta in items]
|