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.
Files changed (38) hide show
  1. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/PKG-INFO +97 -33
  2. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/README.md +96 -32
  3. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/_version.py +3 -3
  4. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/main.py +5 -1
  5. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/css/style.css +285 -31
  6. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/app.js +1706 -794
  7. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/PKG-INFO +97 -33
  8. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/SOURCES.txt +1 -0
  9. devpi_admin-1.4.0/tests/test_devpi_tokens_ui.py +527 -0
  10. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.github/workflows/publish.yml +0 -0
  11. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.github/workflows/tests.yml +0 -0
  12. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/.gitignore +0 -0
  13. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/LICENSE +0 -0
  14. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/__init__.py +0 -0
  15. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/customizer.py +0 -0
  16. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/favicon.svg +0 -0
  17. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/index.html +0 -0
  18. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/api.js +0 -0
  19. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/marked.min.js +0 -0
  20. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/static/js/theme.js +0 -0
  21. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin/tokens.py +0 -0
  22. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/dependency_links.txt +0 -0
  23. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/entry_points.txt +0 -0
  24. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/requires.txt +0 -0
  25. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/devpi_admin.egg-info/top_level.txt +0 -0
  26. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/pyproject.toml +0 -0
  27. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/setup.cfg +0 -0
  28. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/__init__.py +0 -0
  29. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_acl_read.py +0 -0
  30. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_filter.py +0 -0
  31. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_hooks.py +0 -0
  32. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_json_safe.py +0 -0
  33. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_package.py +0 -0
  34. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_pipconf.py +0 -0
  35. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_tokens.py +0 -0
  36. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_tween.py +0 -0
  37. {devpi_admin-1.3.0 → devpi_admin-1.4.0}/tests/test_view_helpers.py +0 -0
  38. {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.0
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** (master only, authenticated users only) - per-replica cards with
42
- authoritative `applied_serial` vs. master serial. Three states:
43
- - **in sync** - replica matches master serial
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 master operators)
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
- - Adaptive kebab menu - items hint whether a token will be issued:
59
- - **`pip.conf`** (public read index, no auth needed) vs. **`pip.conf + token`** (private read)
60
- - **`.pypirc`** (public upload, no auth needed) vs. **`.pypirc + token`** (private upload).
61
- Hidden when nobody can upload (`acl_upload` empty).
62
- - **`pip.conf` modal** - issues a short-lived `read`-scope token bound to the index. Returns
63
- the full `pip.conf` (Copy / Download), a one-off `pip install --index-url ...` command, and
64
- the raw `user:token` pair for `curl`, `devpi login`, etc. Anonymous-readable indexes show
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. Per-index
136
- token list endpoint shows only the caller's own tokens (root sees all).
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) - per-user list with label, **index, scope**,
151
- expiry, issuer, IP; revoke individual or "Reset all". Wide modal layout so the table
152
- doesn't overflow on stage indexes with long names.
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/...`). Master writes to these on every
214
- token issue / revoke. **Replicas without `devpi-admin` installed cannot apply those
215
- changelog entries** - `import_changes` fails with `AssertionError` on the missing
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 master version
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 master. If you upgrade master first and that
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 (master only)
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 master alone can produce.
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 master memory.
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** (master only, authenticated users only) - per-replica cards with
18
- authoritative `applied_serial` vs. master serial. Three states:
19
- - **in sync** - replica matches master serial
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 master operators)
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
- - Adaptive kebab menu - items hint whether a token will be issued:
35
- - **`pip.conf`** (public read index, no auth needed) vs. **`pip.conf + token`** (private read)
36
- - **`.pypirc`** (public upload, no auth needed) vs. **`.pypirc + token`** (private upload).
37
- Hidden when nobody can upload (`acl_upload` empty).
38
- - **`pip.conf` modal** - issues a short-lived `read`-scope token bound to the index. Returns
39
- the full `pip.conf` (Copy / Download), a one-off `pip install --index-url ...` command, and
40
- the raw `user:token` pair for `curl`, `devpi login`, etc. Anonymous-readable indexes show
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. Per-index
112
- token list endpoint shows only the caller's own tokens (root sees all).
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) - per-user list with label, **index, scope**,
127
- expiry, issuer, IP; revoke individual or "Reset all". Wide modal layout so the table
128
- doesn't overflow on stage indexes with long names.
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/...`). Master writes to these on every
190
- token issue / revoke. **Replicas without `devpi-admin` installed cannot apply those
191
- changelog entries** - `import_changes` fails with `AssertionError` on the missing
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 master version
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 master. If you upgrade master first and that
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 (master only)
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 master alone can produce.
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 master memory.
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.3.0'
22
- __version_tuple__ = version_tuple = (1, 3, 0)
21
+ __version__ = version = '1.4.0'
22
+ __version_tuple__ = version_tuple = (1, 4, 0)
23
23
 
24
- __commit_id__ = commit_id = 'gca7f689da'
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
- if auth_user != "root":
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]