devpi-admin 1.3.0__tar.gz → 1.4.1__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.1}/PKG-INFO +103 -33
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/README.md +102 -32
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/_version.py +3 -3
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/main.py +48 -3
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/css/style.css +285 -31
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/js/app.js +1706 -794
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/PKG-INFO +103 -33
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/SOURCES.txt +1 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_acl_read.py +127 -15
- devpi_admin-1.4.1/tests/test_devpi_tokens_ui.py +527 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/.github/workflows/publish.yml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/.github/workflows/tests.yml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/.gitignore +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/LICENSE +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/customizer.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/favicon.svg +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/index.html +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/js/api.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin/tokens.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/dependency_links.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/entry_points.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/requires.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/devpi_admin.egg-info/top_level.txt +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/pyproject.toml +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/setup.cfg +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/__init__.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_filter.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_hooks.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_package.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_pipconf.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_tokens.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_tween.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/tests/test_view_helpers.py +0 -0
- {devpi_admin-1.3.0 → devpi_admin-1.4.1}/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.1
|
|
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
|
|
@@ -128,12 +122,19 @@ talks to the standard devpi JSON API directly.
|
|
|
128
122
|
`DELETE` is **never** granted, even with `upload` scope - package removal must use
|
|
129
123
|
password auth. Anything outside the bound index path returns 403, including the SPA,
|
|
130
124
|
`/+admin-api/*` (so a token cannot mint further tokens), `/+login`, `/`, and `/<user>`.
|
|
125
|
+
**Bases exception**: `GET /<base>/<idx>/+f/<...>` is also allowed when `<base>/<idx>`
|
|
126
|
+
is in the bound stage's SRO (bases inheritance). devpi's `+simple/` view on a stage
|
|
127
|
+
emits file links pointing directly at the base index (e.g. mirror-fed packages on
|
|
128
|
+
`villapro/staging` link to `/root/pypi/+f/...`); without this exception pip would
|
|
129
|
+
follow the link with the bound token and get 403. Limited to `GET` on `+f/` —
|
|
130
|
+
cross-index `+simple/` and writes remain blocked.
|
|
131
131
|
- **Issuance rules**: regular users may issue for themselves; root may issue for *other*
|
|
132
132
|
users (admin delegation) but not for itself. Admin-token-authenticated requests cannot
|
|
133
133
|
issue further tokens. Issuance verifies the target user is in `acl_read` /
|
|
134
134
|
`acl_upload` of the target index.
|
|
135
|
-
- **Management rules**: list / revoke is allowed for the token owner or root.
|
|
136
|
-
token list
|
|
135
|
+
- **Management rules**: list / revoke is allowed for the token owner or root. The
|
|
136
|
+
per-index token list shows all tokens for **index owner / root**; other callers see
|
|
137
|
+
only tokens bound to themselves.
|
|
137
138
|
- **Auto-cleanup**:
|
|
138
139
|
- User delete -> all tokens for that user removed from keyfs
|
|
139
140
|
- Index delete -> all tokens bound to that index removed (USER subscriber diffs the
|
|
@@ -147,9 +148,33 @@ talks to the standard devpi JSON API directly.
|
|
|
147
148
|
|
|
148
149
|
### Users
|
|
149
150
|
- Create, edit (email, password), delete users (admin only)
|
|
150
|
-
- **Tokens manager** (kebab -> Tokens) -
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
- **Tokens manager** (kebab -> Tokens) - unified modal with one or two sections:
|
|
152
|
+
- **Admin tokens** (built-in) — per-user list with label, index, scope, expiry,
|
|
153
|
+
issuer, IP; individual revoke or "Reset all"
|
|
154
|
+
- **Devpi tokens** (only when the `devpi-tokens` plugin is installed) — list of
|
|
155
|
+
macaroon tokens with parsed restrictions (indexes, allowed permissions, projects,
|
|
156
|
+
expires, not-before); individual revoke
|
|
157
|
+
- Empty sections hide automatically (no clutter). Banner above Devpi section
|
|
158
|
+
(dismissible per user) explains the different threat model — raw secret in keyfs
|
|
159
|
+
vs. hash-only Admin storage.
|
|
160
|
+
- **Issue token** (`+ Issue new` button) — single unified modal for both backends:
|
|
161
|
+
- Token type selector at the top picks Admin vs. Devpi (Devpi default when the
|
|
162
|
+
plugin is installed). Hidden when only Admin is available.
|
|
163
|
+
- Index picker shows everything the bound user can access (owns, or appears in
|
|
164
|
+
`acl_read` / `acl_upload`). Devpi uses a multi tag picker; Admin uses a single
|
|
165
|
+
select.
|
|
166
|
+
- Admin scope dropdown adapts to the picked index: public indexes get only
|
|
167
|
+
`upload` (read tokens are useless when anyone can read), private indexes get
|
|
168
|
+
both with `read` as default.
|
|
169
|
+
- Devpi permissions are checkboxes; destructive operations (`del_*`,
|
|
170
|
+
`index_modify`, `index_delete`) are tucked behind an "Advanced" toggle with a
|
|
171
|
+
visual warning.
|
|
172
|
+
- Expiry: presets (1 hour to 1 year) plus a `Custom…` datetime option. Optional
|
|
173
|
+
Not-before for delayed activation (Devpi only).
|
|
174
|
+
- On success the modal swaps to a read-once view with the raw token, pip.conf,
|
|
175
|
+
`.pypirc`, `TWINE_*` env, and a `user:token` pair — but only the configs that
|
|
176
|
+
actually match the issued token's intent (no pip.conf for upload-only or
|
|
177
|
+
public-index tokens; no `.pypirc` for read-only tokens).
|
|
153
178
|
|
|
154
179
|
### Packages
|
|
155
180
|
- Client-side search with PEP 503 name normalization and relevance ranking
|
|
@@ -210,10 +235,10 @@ for HTML requests while `devpi-web` would still serve its own HTML on other rout
|
|
|
210
235
|
### Replicas: install on every node
|
|
211
236
|
|
|
212
237
|
`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.
|
|
238
|
+
`+admin/user-tokens/...`, `+admin/index-tokens/...`). The primary writes to these on
|
|
239
|
+
every token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
240
|
+
those changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
241
|
+
missing keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
217
242
|
|
|
218
243
|
The dashboard's stuck-replica detection is designed exactly for this: a `stuck`
|
|
219
244
|
state on a replica card almost always means a plugin (typically `devpi-admin` itself,
|
|
@@ -222,14 +247,14 @@ is straightforward:
|
|
|
222
247
|
|
|
223
248
|
```bash
|
|
224
249
|
# on the replica
|
|
225
|
-
~/.venv/bin/pip install --upgrade devpi-admin # match
|
|
250
|
+
~/.venv/bin/pip install --upgrade devpi-admin # match primary version
|
|
226
251
|
systemctl restart devpi
|
|
227
252
|
```
|
|
228
253
|
|
|
229
254
|
Replication resumes from the failed serial automatically - no manual keyfs surgery.
|
|
230
255
|
|
|
231
|
-
**Upgrade order:** replicas first, then
|
|
232
|
-
release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
256
|
+
**Upgrade order:** replicas first, then primary. If you upgrade the primary first and
|
|
257
|
+
that release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
233
258
|
|
|
234
259
|
See `INSTALL.md` section 11 for full step-by-step replica setup and dashboard interpretation.
|
|
235
260
|
|
|
@@ -255,6 +280,51 @@ ExecStart=/opt/pypi/venv/bin/devpi-server \
|
|
|
255
280
|
|
|
256
281
|
See `INSTALL.md` for a full systemd unit example.
|
|
257
282
|
|
|
283
|
+
### Optional plugins
|
|
284
|
+
|
|
285
|
+
#### `devpi-tokens` coexistence
|
|
286
|
+
|
|
287
|
+
`devpi-admin` plays nicely with the optional `devpi-tokens` plugin. When
|
|
288
|
+
installed, the SPA detects it (via `/+api` features list and `/+status`
|
|
289
|
+
versioninfo) and **automatically merges Devpi tokens into the same Tokens
|
|
290
|
+
modal** that lists Admin tokens — same kebab item ("Tokens"), same
|
|
291
|
+
"+ Issue new" flow, same per-index Tokens modal. The user picks the
|
|
292
|
+
backend in the Issue form via a token-type selector.
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
/var/lib/pypi/venv/bin/pip install devpi-tokens
|
|
296
|
+
systemctl --user restart devpi
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The two token systems run side by side without conflict:
|
|
300
|
+
|
|
301
|
+
| | Admin tokens | Devpi tokens |
|
|
302
|
+
|---|---|---|
|
|
303
|
+
| Plugin | `devpi-admin` (built-in) | `devpi-tokens` (optional) |
|
|
304
|
+
| Storage | SHA-256 hash in keyfs | Raw HMAC key in keyfs |
|
|
305
|
+
| Listable (incl. derived) | yes, all | initial only — derived macaroons are stateless |
|
|
306
|
+
| Audit log on lookup | yes | no |
|
|
307
|
+
| HTTP method whitelist | `read` blocks DELETE; `upload` blocks DELETE | relies on `--allowed` permission filter |
|
|
308
|
+
| Multi-index per token | no (1:1) | yes |
|
|
309
|
+
| Per-project filter | no | yes (`--projects`) |
|
|
310
|
+
| Cross-user index | no (must be in ACL) | yes (any user/index pair) |
|
|
311
|
+
| CLI compatibility | UI / API only | works with `devpi token-login` |
|
|
312
|
+
|
|
313
|
+
**Threat model note.** Macaroon HMAC verification requires the secret in
|
|
314
|
+
plaintext on the server, so `devpi-tokens` cannot hash-store; a leaked
|
|
315
|
+
backup or replica disk dump exposes working credentials. Prefer Admin
|
|
316
|
+
tokens for privileged workflows. The UI surfaces this via a (dismissible
|
|
317
|
+
per-user) security banner above the Devpi section of every Tokens modal.
|
|
318
|
+
|
|
319
|
+
`acl_read` (provided by `devpi-admin`) applies to both token systems
|
|
320
|
+
identically — devpi evaluates `pkg_read` ACL against whichever identity
|
|
321
|
+
the auth chain produced, regardless of token source.
|
|
322
|
+
|
|
323
|
+
**Testing without the plugin installed.** Append `?no-devpi-tokens` to any
|
|
324
|
+
SPA URL to make the UI behave as if the plugin weren't there (kebab item
|
|
325
|
+
disappears, type selector hides, etc.). Saves you a `pip uninstall + restart`
|
|
326
|
+
round-trip when verifying graceful degradation.
|
|
327
|
+
|
|
258
328
|
## Usage
|
|
259
329
|
|
|
260
330
|
After restart, open:
|
|
@@ -527,7 +597,7 @@ Revoke a single token.
|
|
|
527
597
|
- **200:** `{"revoked": true, "id": "abc..."}`
|
|
528
598
|
- **404:** token id not found
|
|
529
599
|
|
|
530
|
-
### Replication observability (
|
|
600
|
+
### Replication observability (primary only)
|
|
531
601
|
|
|
532
602
|
#### `GET /+admin-api/replicas`
|
|
533
603
|
Last-known poll info per replica, captured from each `GET /+changelog/{N}-` request via
|
|
@@ -538,7 +608,7 @@ applied (`start_serial - 1` from its most recent poll). Compare against `/+statu
|
|
|
538
608
|
Why this isn't `polling_replicas` from `/+status`: devpi-server overwrites
|
|
539
609
|
`xom.polling_replicas[uuid].serial` during streaming and gives a misleading "caught up"
|
|
540
610
|
reading once the response generator drains. Capturing `start_serial` at the request
|
|
541
|
-
boundary is the only stable signal
|
|
611
|
+
boundary is the only stable signal the primary alone can produce.
|
|
542
612
|
|
|
543
613
|
- **Auth:** required
|
|
544
614
|
- **200:**
|
|
@@ -558,7 +628,7 @@ boundary is the only stable signal master alone can produce.
|
|
|
558
628
|
}
|
|
559
629
|
```
|
|
560
630
|
- 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
|
|
631
|
+
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust primary memory.
|
|
562
632
|
|
|
563
633
|
## Project layout
|
|
564
634
|
|
|
@@ -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
|
|
@@ -104,12 +98,19 @@ talks to the standard devpi JSON API directly.
|
|
|
104
98
|
`DELETE` is **never** granted, even with `upload` scope - package removal must use
|
|
105
99
|
password auth. Anything outside the bound index path returns 403, including the SPA,
|
|
106
100
|
`/+admin-api/*` (so a token cannot mint further tokens), `/+login`, `/`, and `/<user>`.
|
|
101
|
+
**Bases exception**: `GET /<base>/<idx>/+f/<...>` is also allowed when `<base>/<idx>`
|
|
102
|
+
is in the bound stage's SRO (bases inheritance). devpi's `+simple/` view on a stage
|
|
103
|
+
emits file links pointing directly at the base index (e.g. mirror-fed packages on
|
|
104
|
+
`villapro/staging` link to `/root/pypi/+f/...`); without this exception pip would
|
|
105
|
+
follow the link with the bound token and get 403. Limited to `GET` on `+f/` —
|
|
106
|
+
cross-index `+simple/` and writes remain blocked.
|
|
107
107
|
- **Issuance rules**: regular users may issue for themselves; root may issue for *other*
|
|
108
108
|
users (admin delegation) but not for itself. Admin-token-authenticated requests cannot
|
|
109
109
|
issue further tokens. Issuance verifies the target user is in `acl_read` /
|
|
110
110
|
`acl_upload` of the target index.
|
|
111
|
-
- **Management rules**: list / revoke is allowed for the token owner or root.
|
|
112
|
-
token list
|
|
111
|
+
- **Management rules**: list / revoke is allowed for the token owner or root. The
|
|
112
|
+
per-index token list shows all tokens for **index owner / root**; other callers see
|
|
113
|
+
only tokens bound to themselves.
|
|
113
114
|
- **Auto-cleanup**:
|
|
114
115
|
- User delete -> all tokens for that user removed from keyfs
|
|
115
116
|
- Index delete -> all tokens bound to that index removed (USER subscriber diffs the
|
|
@@ -123,9 +124,33 @@ talks to the standard devpi JSON API directly.
|
|
|
123
124
|
|
|
124
125
|
### Users
|
|
125
126
|
- Create, edit (email, password), delete users (admin only)
|
|
126
|
-
- **Tokens manager** (kebab -> Tokens) -
|
|
127
|
-
|
|
128
|
-
|
|
127
|
+
- **Tokens manager** (kebab -> Tokens) - unified modal with one or two sections:
|
|
128
|
+
- **Admin tokens** (built-in) — per-user list with label, index, scope, expiry,
|
|
129
|
+
issuer, IP; individual revoke or "Reset all"
|
|
130
|
+
- **Devpi tokens** (only when the `devpi-tokens` plugin is installed) — list of
|
|
131
|
+
macaroon tokens with parsed restrictions (indexes, allowed permissions, projects,
|
|
132
|
+
expires, not-before); individual revoke
|
|
133
|
+
- Empty sections hide automatically (no clutter). Banner above Devpi section
|
|
134
|
+
(dismissible per user) explains the different threat model — raw secret in keyfs
|
|
135
|
+
vs. hash-only Admin storage.
|
|
136
|
+
- **Issue token** (`+ Issue new` button) — single unified modal for both backends:
|
|
137
|
+
- Token type selector at the top picks Admin vs. Devpi (Devpi default when the
|
|
138
|
+
plugin is installed). Hidden when only Admin is available.
|
|
139
|
+
- Index picker shows everything the bound user can access (owns, or appears in
|
|
140
|
+
`acl_read` / `acl_upload`). Devpi uses a multi tag picker; Admin uses a single
|
|
141
|
+
select.
|
|
142
|
+
- Admin scope dropdown adapts to the picked index: public indexes get only
|
|
143
|
+
`upload` (read tokens are useless when anyone can read), private indexes get
|
|
144
|
+
both with `read` as default.
|
|
145
|
+
- Devpi permissions are checkboxes; destructive operations (`del_*`,
|
|
146
|
+
`index_modify`, `index_delete`) are tucked behind an "Advanced" toggle with a
|
|
147
|
+
visual warning.
|
|
148
|
+
- Expiry: presets (1 hour to 1 year) plus a `Custom…` datetime option. Optional
|
|
149
|
+
Not-before for delayed activation (Devpi only).
|
|
150
|
+
- On success the modal swaps to a read-once view with the raw token, pip.conf,
|
|
151
|
+
`.pypirc`, `TWINE_*` env, and a `user:token` pair — but only the configs that
|
|
152
|
+
actually match the issued token's intent (no pip.conf for upload-only or
|
|
153
|
+
public-index tokens; no `.pypirc` for read-only tokens).
|
|
129
154
|
|
|
130
155
|
### Packages
|
|
131
156
|
- Client-side search with PEP 503 name normalization and relevance ranking
|
|
@@ -186,10 +211,10 @@ for HTML requests while `devpi-web` would still serve its own HTML on other rout
|
|
|
186
211
|
### Replicas: install on every node
|
|
187
212
|
|
|
188
213
|
`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.
|
|
214
|
+
`+admin/user-tokens/...`, `+admin/index-tokens/...`). The primary writes to these on
|
|
215
|
+
every token issue / revoke. **Replicas without `devpi-admin` installed cannot apply
|
|
216
|
+
those changelog entries** - `import_changes` fails with `AssertionError` on the
|
|
217
|
+
missing keyfs key, the replica rolls back to the prior serial, and replication stalls.
|
|
193
218
|
|
|
194
219
|
The dashboard's stuck-replica detection is designed exactly for this: a `stuck`
|
|
195
220
|
state on a replica card almost always means a plugin (typically `devpi-admin` itself,
|
|
@@ -198,14 +223,14 @@ is straightforward:
|
|
|
198
223
|
|
|
199
224
|
```bash
|
|
200
225
|
# on the replica
|
|
201
|
-
~/.venv/bin/pip install --upgrade devpi-admin # match
|
|
226
|
+
~/.venv/bin/pip install --upgrade devpi-admin # match primary version
|
|
202
227
|
systemctl restart devpi
|
|
203
228
|
```
|
|
204
229
|
|
|
205
230
|
Replication resumes from the failed serial automatically - no manual keyfs surgery.
|
|
206
231
|
|
|
207
|
-
**Upgrade order:** replicas first, then
|
|
208
|
-
release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
232
|
+
**Upgrade order:** replicas first, then primary. If you upgrade the primary first and
|
|
233
|
+
that release introduces a new keyfs key, replicas would crash on the very next poll.
|
|
209
234
|
|
|
210
235
|
See `INSTALL.md` section 11 for full step-by-step replica setup and dashboard interpretation.
|
|
211
236
|
|
|
@@ -231,6 +256,51 @@ ExecStart=/opt/pypi/venv/bin/devpi-server \
|
|
|
231
256
|
|
|
232
257
|
See `INSTALL.md` for a full systemd unit example.
|
|
233
258
|
|
|
259
|
+
### Optional plugins
|
|
260
|
+
|
|
261
|
+
#### `devpi-tokens` coexistence
|
|
262
|
+
|
|
263
|
+
`devpi-admin` plays nicely with the optional `devpi-tokens` plugin. When
|
|
264
|
+
installed, the SPA detects it (via `/+api` features list and `/+status`
|
|
265
|
+
versioninfo) and **automatically merges Devpi tokens into the same Tokens
|
|
266
|
+
modal** that lists Admin tokens — same kebab item ("Tokens"), same
|
|
267
|
+
"+ Issue new" flow, same per-index Tokens modal. The user picks the
|
|
268
|
+
backend in the Issue form via a token-type selector.
|
|
269
|
+
|
|
270
|
+
```bash
|
|
271
|
+
/var/lib/pypi/venv/bin/pip install devpi-tokens
|
|
272
|
+
systemctl --user restart devpi
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The two token systems run side by side without conflict:
|
|
276
|
+
|
|
277
|
+
| | Admin tokens | Devpi tokens |
|
|
278
|
+
|---|---|---|
|
|
279
|
+
| Plugin | `devpi-admin` (built-in) | `devpi-tokens` (optional) |
|
|
280
|
+
| Storage | SHA-256 hash in keyfs | Raw HMAC key in keyfs |
|
|
281
|
+
| Listable (incl. derived) | yes, all | initial only — derived macaroons are stateless |
|
|
282
|
+
| Audit log on lookup | yes | no |
|
|
283
|
+
| HTTP method whitelist | `read` blocks DELETE; `upload` blocks DELETE | relies on `--allowed` permission filter |
|
|
284
|
+
| Multi-index per token | no (1:1) | yes |
|
|
285
|
+
| Per-project filter | no | yes (`--projects`) |
|
|
286
|
+
| Cross-user index | no (must be in ACL) | yes (any user/index pair) |
|
|
287
|
+
| CLI compatibility | UI / API only | works with `devpi token-login` |
|
|
288
|
+
|
|
289
|
+
**Threat model note.** Macaroon HMAC verification requires the secret in
|
|
290
|
+
plaintext on the server, so `devpi-tokens` cannot hash-store; a leaked
|
|
291
|
+
backup or replica disk dump exposes working credentials. Prefer Admin
|
|
292
|
+
tokens for privileged workflows. The UI surfaces this via a (dismissible
|
|
293
|
+
per-user) security banner above the Devpi section of every Tokens modal.
|
|
294
|
+
|
|
295
|
+
`acl_read` (provided by `devpi-admin`) applies to both token systems
|
|
296
|
+
identically — devpi evaluates `pkg_read` ACL against whichever identity
|
|
297
|
+
the auth chain produced, regardless of token source.
|
|
298
|
+
|
|
299
|
+
**Testing without the plugin installed.** Append `?no-devpi-tokens` to any
|
|
300
|
+
SPA URL to make the UI behave as if the plugin weren't there (kebab item
|
|
301
|
+
disappears, type selector hides, etc.). Saves you a `pip uninstall + restart`
|
|
302
|
+
round-trip when verifying graceful degradation.
|
|
303
|
+
|
|
234
304
|
## Usage
|
|
235
305
|
|
|
236
306
|
After restart, open:
|
|
@@ -503,7 +573,7 @@ Revoke a single token.
|
|
|
503
573
|
- **200:** `{"revoked": true, "id": "abc..."}`
|
|
504
574
|
- **404:** token id not found
|
|
505
575
|
|
|
506
|
-
### Replication observability (
|
|
576
|
+
### Replication observability (primary only)
|
|
507
577
|
|
|
508
578
|
#### `GET /+admin-api/replicas`
|
|
509
579
|
Last-known poll info per replica, captured from each `GET /+changelog/{N}-` request via
|
|
@@ -514,7 +584,7 @@ applied (`start_serial - 1` from its most recent poll). Compare against `/+statu
|
|
|
514
584
|
Why this isn't `polling_replicas` from `/+status`: devpi-server overwrites
|
|
515
585
|
`xom.polling_replicas[uuid].serial` during streaming and gives a misleading "caught up"
|
|
516
586
|
reading once the response generator drains. Capturing `start_serial` at the request
|
|
517
|
-
boundary is the only stable signal
|
|
587
|
+
boundary is the only stable signal the primary alone can produce.
|
|
518
588
|
|
|
519
589
|
- **Auth:** required
|
|
520
590
|
- **200:**
|
|
@@ -534,7 +604,7 @@ boundary is the only stable signal master alone can produce.
|
|
|
534
604
|
}
|
|
535
605
|
```
|
|
536
606
|
- 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
|
|
607
|
+
(least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust primary memory.
|
|
538
608
|
|
|
539
609
|
## Project layout
|
|
540
610
|
|
|
@@ -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.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 1)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'gf69f25fba'
|
|
@@ -444,7 +444,7 @@ def devpi_admin_tween_factory(handler, registry):
|
|
|
444
444
|
# Cache for the identity hook so it doesn't re-read keyfs.
|
|
445
445
|
request.environ["adm.token_meta"] = meta
|
|
446
446
|
request.environ["adm.is_admin_token"] = True
|
|
447
|
-
denied = _admin_token_check(request, meta)
|
|
447
|
+
denied = _admin_token_check(request, meta, xom)
|
|
448
448
|
if denied is not None:
|
|
449
449
|
return denied
|
|
450
450
|
# Invalid/expired token: let the identity hook return None and
|
|
@@ -510,7 +510,7 @@ def _request_carries_admin_token(request):
|
|
|
510
510
|
return _extract_admin_token_secret(request) is not None
|
|
511
511
|
|
|
512
512
|
|
|
513
|
-
def _admin_token_check(request, token_meta):
|
|
513
|
+
def _admin_token_check(request, token_meta, xom):
|
|
514
514
|
"""Restrict admin-token requests by scope and bound index.
|
|
515
515
|
|
|
516
516
|
A token carries a ``scope`` (``read`` / ``upload``) and is bound to a
|
|
@@ -523,6 +523,13 @@ def _admin_token_check(request, token_meta):
|
|
|
523
523
|
``/<user>/<index>/...``. A token bound to ``alice/dev`` cannot
|
|
524
524
|
reach ``/bob/prod`` or any management endpoint (``/+admin*``,
|
|
525
525
|
``/+admin-api/*``, ``/+login``, ``/+status``, ``/<user>``).
|
|
526
|
+
* Cross-base file downloads (``GET /<base>/<idx>/+f/...``) are
|
|
527
|
+
allowed when the target index is reachable through the bound
|
|
528
|
+
stage's SRO (bases inheritance). devpi's ``+simple/`` on a stage
|
|
529
|
+
returns links pointing to the base index hosting the file (e.g.
|
|
530
|
+
``/root/pypi/+f/...`` for a mirror-fed package on
|
|
531
|
+
``villapro/staging``); without this exception pip would follow
|
|
532
|
+
the link with the bound token and get 403.
|
|
526
533
|
|
|
527
534
|
Returns ``None`` to allow, or an HTTPForbidden response to deny.
|
|
528
535
|
"""
|
|
@@ -553,11 +560,45 @@ def _admin_token_check(request, token_meta):
|
|
|
553
560
|
prefix = "/" + bound
|
|
554
561
|
if path == prefix or path.startswith(prefix + "/"):
|
|
555
562
|
return None
|
|
563
|
+
# Bases inheritance for file downloads: a stage's +simple/ surfaces
|
|
564
|
+
# links into the base index (e.g. mirror) rather than rewriting them
|
|
565
|
+
# under the stage URL. Allow the resulting cross-index GET as long
|
|
566
|
+
# as the target is part of the bound stage's SRO.
|
|
567
|
+
if request.method == "GET":
|
|
568
|
+
file_match = _PKG_FILE_RE.match(path)
|
|
569
|
+
if file_match is not None:
|
|
570
|
+
other_user, other_index = file_match.group(1), file_match.group(2)
|
|
571
|
+
if _index_in_bound_sro(xom, bound, other_user, other_index):
|
|
572
|
+
return None
|
|
556
573
|
return HTTPForbidden(json_body={
|
|
557
574
|
"error": f"admin token bound to {bound} cannot access {path}",
|
|
558
575
|
})
|
|
559
576
|
|
|
560
577
|
|
|
578
|
+
def _index_in_bound_sro(xom, bound, other_user, other_index):
|
|
579
|
+
"""Return True iff ``other_user/other_index`` is in bound stage's SRO.
|
|
580
|
+
|
|
581
|
+
SRO (Stage Resolution Order) is devpi's transitive bases traversal —
|
|
582
|
+
the same chain its ``+simple/`` view follows when emitting links.
|
|
583
|
+
Best-effort: missing stage or any error returns False (deny). Runs
|
|
584
|
+
inside a read transaction because ``sro()`` touches each base's
|
|
585
|
+
``ixconfig`` in keyfs.
|
|
586
|
+
"""
|
|
587
|
+
bound_user, bound_idx = bound.split("/", 1)
|
|
588
|
+
try:
|
|
589
|
+
with xom.keyfs.read_transaction(allow_reuse=True):
|
|
590
|
+
bound_stage = xom.model.getstage(bound_user, bound_idx)
|
|
591
|
+
if bound_stage is None:
|
|
592
|
+
return False
|
|
593
|
+
target_name = f"{other_user}/{other_index}"
|
|
594
|
+
for stage in bound_stage.sro():
|
|
595
|
+
if stage.name == target_name:
|
|
596
|
+
return True
|
|
597
|
+
except Exception:
|
|
598
|
+
return False
|
|
599
|
+
return False
|
|
600
|
+
|
|
601
|
+
|
|
561
602
|
def _user_listing_check(request):
|
|
562
603
|
"""Restrict ``GET /<user>/`` to that user or root.
|
|
563
604
|
|
|
@@ -1345,7 +1386,11 @@ def _list_index_tokens_view(request):
|
|
|
1345
1386
|
# Hide existence from anyone without read access.
|
|
1346
1387
|
raise HTTPNotFound(json_body={"error": "index not found"})
|
|
1347
1388
|
items = tokens.list_for_index(xom, idx_user, idx_name)
|
|
1348
|
-
|
|
1389
|
+
# Index owner / root see every token bound to this index — they're
|
|
1390
|
+
# the people responsible for revoke decisions and need full audit
|
|
1391
|
+
# visibility. Non-owners see only their own tokens (the bound user
|
|
1392
|
+
# they themselves authenticate as), matching the per-user listing.
|
|
1393
|
+
if auth_user != "root" and auth_user != idx_user:
|
|
1349
1394
|
items = [(tid, meta) for tid, meta in items
|
|
1350
1395
|
if meta.get("user") == auth_user]
|
|
1351
1396
|
result = [_format_token_record(tid, meta) for tid, meta in items]
|