Modern web UI plugin for devpi-server — drop-in replacement for devpi-web
Project description
devpi-admin
A modern web UI plugin for devpi-server - a drop-in replacement for
devpi-web. Ships as a Python package that registers itself as a devpi-server plugin via the
standard entry point mechanism, so a single pip install devpi-admin is enough.
The UI itself is a bundled single-page application (pure HTML + CSS + vanilla JavaScript, no
build step) served under /+admin/. All devpi REST API endpoints remain untouched - the SPA
talks to the standard devpi JSON API directly.
Features
Dashboard
- Server info with version of devpi-server and all installed plugins (auto-detected)
- Cache metrics with hit-rate bars (storage, changelog, relpath caches)
- Whoosh search index queue status
- Replica status (primary only, authenticated users only) - per-replica cards with
authoritative
applied_serialvs. primary serial. Three states:- in sync - replica matches primary serial
- lagging - replica is behind but advancing
- stuck - replica has been polling the same serial for >=30 s; usually means a
server-side plugin (
devpi-admin,devpi-web, ...) is missing or out of date on the replica
- Topbar health indicator - the
devpi adminlogo is coloured green / orange / red on every page, refreshed every 30 s in the background:- server reachable, all replicas in sync
- at least one replica lagging (visible to authenticated primary operators)
- server not responding
Indexes
- Visual cards color-coded by type: green (stage), amber (volatile stage), blue (mirror)
- Warning tags for ACL edge cases:
world-writable-acl_uploadcontains:ANONYMOUS:; supply-chain riskno upload-acl_uploadis empty; nobody (not even owner / root) can publish
- Index card kebab:
pip.conf(public indexes only) — static one-click pip.conf with the index URL, no token issuance neededTokens(owner / root only) — opens the per-index unified Tokens modal with two sections (Admin + Devpi), shows existing tokens for this index, lets you issue new ones with the index pre-filled and lockedEdit/Delete(owner / root)
- Create / edit / delete indexes via modal dialogs
baseseditor with drag & drop priority ordering and transitive inheritance displayacl_uploadandacl_readtag pickers with user selection dropdownvolatile,mirror_url,titleconfiguration- Mirror package allow/deny lists (
package_allowlist,package_denylist) — see Mirror access control below
Read access control (acl_read)
- Per-index list of principals allowed to read the index (download packages, browse simple)
- Default
[:ANONYMOUS:]- public, behaves like devpi-web - Set to specific users (
alice,bob) to make the index private - Special principals:
:ANONYMOUS:(everyone, including unauthenticated) and:AUTHENTICATED:(any logged-in user) - Enforced natively by devpi via the
pkg_readpermission on every download path, plus a tween that filters out invisible indexes from the root listing (GET /) and rejects direct access to private indexes with 404
Mirror access control (allow/deny lists)
- Per-mirror
package_allowlistandpackage_denylistfilter the projects, versions and simple-index links served from upstream. Onlytype=mirrorindexes carry these fields; stage indexes are unaffected - Empty allowlist = pass-through (everything allowed except denylist). Non-empty allowlist = whitelist mode (only listed entries reach pip)
- Denylist always wins — overrides any allowlist match
- Entry formats (one per line in the modal):
- PEP 508 requirement —
numpy,numpy>=2.0,urllib3<1.26.5 - Glob in name part —
mycompany-*,*-internal,mycompany-*<2.0
- PEP 508 requirement —
- Multi-layer enforcement so a denylist hit cannot be bypassed:
+simple/<project>/— denied versions never appear in pip's discovery (devpi-server's customizer hooks:get_projects_filter_iter,get_versions_filter_iter,get_simple_links_filter_iter)/<user>/<index>listing — denied projects vanish from the project list+f/<hash>/<filename>direct download — tween returns 404 even for previously cached files (defense in depth against shared/bookmarked URLs). The cached file stays on disk; removing the deny rule restores access without re-fetching upstream
- Use cases:
- CVE blocklist —
urllib3<1.26.5,cryptography<41.0.0 - Internal namespace ban —
mycompany-*keeps PyPI typosquats from shadowing private packages on a public mirror - Whitelist-only mirrors — paste curated
requirements.txtstyle entries intopackage_allowlist; everything else is blocked
- CVE blocklist —
Admin tokens (scoped, revocable)
-
Opaque
adm_<id>.<secret>tokens bound to a(user, index, scope)triple. Scope isread(pip install) orupload(twine /devpi upload). A leaked token is contained to one index and one operation class - no cross-index or upgrade path. -
Tokens are persisted in keyfs as SHA-256 hashes only - the plaintext secret is shown exactly once at issuance. A keyfs dump (replica disk, backup) does not yield usable tokens. Lookup compares hashes via
hmac.compare_digest(constant-time). -
TTL configurable per-token (60 s up to 1 year), uniquely revocable
-
Tween enforcement matrix:
scope allowed methods allowed paths readGET, HEAD /+api,/<token.user>/<token.index>/...uploadGET, HEAD, POST, PUT /+api,/<token.user>/<token.index>/...DELETEis never granted, even withuploadscope - package removal must use password auth. Anything outside the bound index path returns 403, including the SPA,/+admin-api/*(so a token cannot mint further tokens),/+login,/, and/<user>. Bases exception:GET /<base>/<idx>/+f/<...>is also allowed when<base>/<idx>is in the bound stage's SRO (bases inheritance). devpi's+simple/view on a stage emits file links pointing directly at the base index (e.g. mirror-fed packages onvillapro/staginglink to/root/pypi/+f/...); without this exception pip would follow the link with the bound token and get 403. Limited toGETon+f/— cross-index+simple/and writes remain blocked. -
Issuance rules: regular users may issue for themselves; root may issue for other users (admin delegation) but not for itself. Admin-token-authenticated requests cannot issue further tokens. Issuance verifies the target user is in
acl_read/acl_uploadof the target index. -
Management rules: list / revoke is allowed for the token owner or root. The per-index token list shows all tokens for index owner / root; other callers see only tokens bound to themselves.
-
Auto-cleanup:
- User delete -> all tokens for that user removed from keyfs
- Index delete -> all tokens bound to that index removed (USER subscriber diffs the
indexesdict) - Legacy tokens (pre-hash storage, or pre-
index/scope) wiped at startup
-
Audit log: failed lookups (unknown id, secret mismatch, expired, deleted user, legacy token) are logged at WARNING/INFO so an operator can spot bruteforce attempts.
-
CI/Ansible-friendly:
GET /+admin-api/pip-conf?index=user/index&ttl=3600returns a ready-to-usepip.conf(text/plain) in one HTTP call. For upload, usePOST /+admin-api/tokenwith{"scope": "upload"}.
Users
- Create, edit (email, password), delete users (admin only)
- Tokens manager (kebab -> Tokens) - unified modal with one or two sections:
- Admin tokens (built-in) — per-user list with label, index, scope, expiry, issuer, IP; individual revoke or "Reset all"
- Devpi tokens (only when the
devpi-tokensplugin is installed) — list of macaroon tokens with parsed restrictions (indexes, allowed permissions, projects, expires, not-before); individual revoke - Empty sections hide automatically (no clutter). Banner above Devpi section (dismissible per user) explains the different threat model — raw secret in keyfs vs. hash-only Admin storage.
- Issue token (
+ Issue newbutton) — single unified modal for both backends:- Token type selector at the top picks Admin vs. Devpi (Devpi default when the plugin is installed). Hidden when only Admin is available.
- Index picker shows everything the bound user can access (owns, or appears in
acl_read/acl_upload). Devpi uses a multi tag picker; Admin uses a single select. - Admin scope dropdown adapts to the picked index: public indexes get only
upload(read tokens are useless when anyone can read), private indexes get both withreadas default. - Devpi permissions are checkboxes; destructive operations (
del_*,index_modify,index_delete) are tucked behind an "Advanced" toggle with a visual warning. - Expiry: presets (1 hour to 1 year) plus a
Custom…datetime option. Optional Not-before for delayed activation (Devpi only). - On success the modal swaps to a read-once view with the raw token, pip.conf,
.pypirc,TWINE_*env, and auser:tokenpair — but only the configs that actually match the issued token's intent (no pip.conf for upload-only or public-index tokens; no.pypircfor read-only tokens).
Packages
- Client-side search with PEP 503 name normalization and relevance ranking
(exact match > prefix match > substring match, then shortest name first) so
searching
requestsin a 780k-project upstream surfacesrequestsitself, notdjango-requests-cachefirst - Stage indexes load packages automatically. Mirror indexes (e.g.
root/pypi≈ 780k upstream projects, ~17 MB) require an explicit "Browse full index" click — no auto-fetch - Package cards with latest version and
pip installcommand
Package detail (PyPI-like layout)
- Sidebar: metadata (author, license, Python version, keywords, platform, maintainer,
extras, project URLs, dependencies),
pip installcommand, file downloads with upload dates - Version list: every known version of the package, newest first, each linking to its own detail view
- README: rendered markdown (via
marked.js); fetched from PyPI.org for mirror packages where devpi doesn't cache the description
General
- Anonymous browsing - visitors can explore public indexes without logging in; admin
actions (create/edit/delete) appear only after authentication. Private indexes
(
acl_readwithout:ANONYMOUS:) are hidden from anonymous root listing. - Hardened SPA delivery - strict
Content-Security-Policy(no inline scripts,connect-srclimited to same-origin +pypi.org,frame-ancestors 'none'),X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer. Markdown READMEs are sanitised before rendering (script/iframe/event handlers stripped, dangerous URL schemes blocked). - Dark / light / auto theme with half-circle icon for auto mode
- Responsive mobile menu with hamburger toggle
- ESC + outside-click dismissal for modals, dropdown menus, mobile menu
- Login via modal - no separate login page
Installation
pip install devpi-admin
This pulls in devpi-server as a dependency. If you are using devpi in a dedicated venv
(recommended), install the plugin into the same venv:
/var/lib/pypi/venv/bin/pip install devpi-admin
systemctl --user restart devpi # or however you run devpi-server
You should uninstall devpi-web - devpi-admin replaces it entirely:
pip uninstall devpi-web
Both plugins can technically coexist but it is not recommended. devpi-admin intercepts /
for HTML requests while devpi-web would still serve its own HTML on other routes like
/<user>/<index>/<package>, leading to a confusing mixed experience.
Replicas: install on every node
devpi-admin registers custom keyfs keys (+admin/tokens/...,
+admin/user-tokens/..., +admin/index-tokens/...). The primary writes to these on
every token issue / revoke. Replicas without devpi-admin installed cannot apply
those changelog entries - import_changes fails with AssertionError on the
missing keyfs key, the replica rolls back to the prior serial, and replication stalls.
The dashboard's stuck-replica detection is designed exactly for this: a stuck
state on a replica card almost always means a plugin (typically devpi-admin itself,
also devpi-web, devpi-postgresql) is missing or out of date on the replica. Recovery
is straightforward:
# on the replica
~/.venv/bin/pip install --upgrade devpi-admin # match primary version
systemctl restart devpi
Replication resumes from the failed serial automatically - no manual keyfs surgery.
Upgrade order: replicas first, then primary. If you upgrade the primary first and that release introduces a new keyfs key, replicas would crash on the very next poll.
See INSTALL.md section 11 for full step-by-step replica setup and dashboard interpretation.
Recommended for production: --restrict-modify root
devpi-server starts in an open mode by default - anyone (including unauthenticated
clients) can PUT /<newuser> to create an account, and any logged-in user can
PUT /<user>/<index> to spin up indexes under their own account. The devpi-admin UI
hides those buttons from non-root users, but a direct API call (curl, devpi user -c)
will still succeed.
Pass --restrict-modify root to devpi-server to lock structural operations
(create/modify/delete of users and indexes) down to root only. Per-index
acl_upload/acl_read are unaffected, so day-to-day uploads and downloads keep working
under the existing per-index permissions.
ExecStart=/opt/pypi/venv/bin/devpi-server \
--serverdir /var/lib/pypi/data \
--restrict-modify root \
...
See INSTALL.md for a full systemd unit example.
Optional plugins
devpi-tokens coexistence
devpi-admin plays nicely with the optional devpi-tokens plugin. When
installed, the SPA detects it (via /+api features list and /+status
versioninfo) and automatically merges Devpi tokens into the same Tokens
modal that lists Admin tokens — same kebab item ("Tokens"), same
"+ Issue new" flow, same per-index Tokens modal. The user picks the
backend in the Issue form via a token-type selector.
/var/lib/pypi/venv/bin/pip install devpi-tokens
systemctl --user restart devpi
The two token systems run side by side without conflict:
| Admin tokens | Devpi tokens | |
|---|---|---|
| Plugin | devpi-admin (built-in) |
devpi-tokens (optional) |
| Storage | SHA-256 hash in keyfs | Raw HMAC key in keyfs |
| Listable (incl. derived) | yes, all | initial only — derived macaroons are stateless |
| Audit log on lookup | yes | no |
| HTTP method whitelist | read blocks DELETE; upload blocks DELETE |
relies on --allowed permission filter |
| Multi-index per token | no (1:1) | yes |
| Per-project filter | no | yes (--projects) |
| Cross-user index | no (must be in ACL) | yes (any user/index pair) |
| CLI compatibility | UI / API only | works with devpi token-login |
Threat model note. Macaroon HMAC verification requires the secret in
plaintext on the server, so devpi-tokens cannot hash-store; a leaked
backup or replica disk dump exposes working credentials. Prefer Admin
tokens for privileged workflows. The UI surfaces this via a (dismissible
per-user) security banner above the Devpi section of every Tokens modal.
acl_read (provided by devpi-admin) applies to both token systems
identically — devpi evaluates pkg_read ACL against whichever identity
the auth chain produced, regardless of token source.
Testing without the plugin installed. Append ?no-devpi-tokens to any
SPA URL to make the UI behave as if the plugin weren't there (kebab item
disappears, type selector hides, etc.). Saves you a pip uninstall + restart
round-trip when verifying graceful degradation.
Usage
After restart, open:
http://<your-devpi-host>:3141/
Browser visits to / are redirected to /+admin/, which serves the SPA. Direct links like
http://<host>:3141/+admin/#packages/ci/testing work and can be bookmarked.
devpi CLI tools and other JSON clients are unaffected - they send Accept: application/json
and bypass the redirect.
CI/Ansible: short-lived pip.conf via the API
For automation that needs to install from a private index, store the service user's password
as a secret and let the pipeline mint a fresh short-lived pip.conf per run:
# Gitea Actions example
- name: Install dependencies
env:
DEVPI_USER: ${{ secrets.DEVPI_USER }} # e.g. "gitea-ci"
DEVPI_PASSWORD: ${{ secrets.DEVPI_PASSWORD }}
run: |
mkdir -p ~/.pip
AUTH=$(printf '%s:%s' "$DEVPI_USER" "$DEVPI_PASSWORD" | base64)
curl -sf -H "X-Devpi-Auth: $AUTH" \
"https://devpi.example.com/+admin-api/pip-conf?index=company/private&ttl=3600&wait_replicas=10" \
> ~/.pip/pip.conf
pip install -r requirements.txt
Replication race: wait_replicas
When devpi runs as primary + replicas behind a load balancer, a freshly issued token
exists on the primary instantly but takes one polling cycle (~37 s by default) to reach
replicas. An Ansible-style playbook that issues a token and immediately uses it through
the LB may hit a replica that doesn't know the token yet - and get 401.
Both POST /+admin-api/token and GET /+admin-api/pip-conf accept a wait_replicas
parameter. The primary blocks until every currently-polling replica has caught up to
the commit serial, bounded by 30 s. Stale replicas (silent for >2 min) are skipped so an
offline replica never blocks the caller.
# Wait up to 10 s for replicas; default cap is 30 s if you pass `true`/`1`.
curl -sf -H "X-Devpi-Auth: $AUTH" \
"https://devpi.example.com/+admin-api/pip-conf?index=company/private&ttl=3600&wait_replicas=10" \
> ~/.pip/pip.conf
For POST /+admin-api/token, send {"wait_replicas": 10} in the JSON body. The response
includes a replication block (synced, waited, timed_out, replicas, ...) so the
client can decide whether to retry.
The token issued is read-scoped - usable only for GET/HEAD on /+api and the
bound /<user>/<index>/.... It cannot upload, modify indexes, change passwords,
exchange itself for a session token, or issue another token. It expires after ttl
seconds. The service user must have pkg_read on the target index. Root may issue
for other users (admin delegation) but never for itself.
For uploads, POST /+admin-api/token with {"scope": "upload"} returns a token that
adds POST/PUT to the bound index - usable from twine or devpi upload:
# CI publish step
- name: Publish wheel
run: |
AUTH=$(printf '%s:%s' "$DEVPI_USER" "$DEVPI_PASSWORD" | base64)
TOKEN=$(curl -sf -H "X-Devpi-Auth: $AUTH" -H 'Content-Type: application/json' \
-d '{"index":"company/release","scope":"upload","ttl_seconds":900}' \
"https://devpi.example.com/+admin-api/token" | jq -r .token)
twine upload --repository-url https://devpi.example.com/company/release/ \
-u "$DEVPI_USER" -p "$TOKEN" dist/*
Upload tokens still cannot DELETE - package removal must use password auth.
Trusted proxy for client IP logging
The client_ip field on issued tokens (visible in the token list) is taken from
request.client_addr by default. When devpi-server runs behind a reverse proxy, set
DEVPI_ADMIN_TRUSTED_PROXIES to a comma-separated list of CIDRs whose X-Forwarded-For
header should be honoured:
DEVPI_ADMIN_TRUSTED_PROXIES=10.0.0.0/8,127.0.0.1
Without this variable, X-Forwarded-For is ignored - preventing clients from forging
their logged IP.
How it works
devpi-admin registers a devpi_server entry point with several @hookimpls:
devpiserver_get_features- advertises the plugin in/+api.devpiserver_indexconfig_defaults- registersacl_readas an indexconfig field with anACLListmarker so devpi normalizes its values on everyPUT/PATCH.devpiserver_stage_get_principals_for_pkg_read- feedsacl_readinto devpi's pyramid ACL, which applies thepkg_readpermission natively on every download path (+f/,+e/, simple page).devpiserver_get_identity- recognizesadm_<id>.<secret>admin tokens, validates them against keyfs (constant-time hash compare), setsadm.is_admin_tokenin the request environ for downstream tween checks.devpiserver_pyramid_configure- registers the SPA, custom API views, the tween, the token keyfs keys, and a USER-key subscriber that cleans up tokens on user delete AND on per-user-index removal (diffs old vs. newindexesdict viatx.get_value_at). Primary only - replicas are read-only.
The tween does several things on every request:
- Captures replica poll info. Matches
GET /+changelog/{N}-?and recordsstart_serial+last_seenkeyed by theX-DEVPI-REPLICA-UUIDheader. This is the data source for/+admin-api/replicasand the dashboard's stuck-replica detection. - Validates admin tokens by direct
tokens.lookup()(not via pyramid identity, which would pin a stale identity through/+login's mid-request header swap). On valid token, setsadm.token_metain the request environ for the identity hook to reuse. - Enforces token scope and index binding:
readscope -> only GET/HEAD alloweduploadscope -> adds POST/PUT (DELETE is never granted)- URL must be
/+apior under/<token.user>/<token.index>/.... Anything else (other indexes, SPA,/+admin-api/*,/+login, root listing,/<user>) returns 403.
- Redirects HTML browser requests on
/to/+admin/while leaving JSON requests intact. - Returns 404 for
GET /<user>/<index>/...(index, simple, project, version, file) when the requestor lackspkg_read- devpi's own listing endpoints have no permission check, so we add one. - Returns 403/404 for
GET /<user>when the requestor is neither the user themselves norroot- devpi otherwise leaks the full list of that user's private indexes. - Filters the
GET /JSON response to remove indexes the requestor can't read, and addsCache-Control: private, no-storeso a shared cache cannot serve one user's filtered view to another.
The SPA HTML (/+admin/) is served with security headers - strict
Content-Security-Policy (no inline scripts, restricted connect-src to
'self' + https://pypi.org for the README fallback, frame-ancestors 'none'),
plus X-Content-Type-Options: nosniff and Referrer-Policy: no-referrer.
The plugin uses devpi-server internals: xom.model.getstage, stage.list_versions,
stage.get_versiondata, stage.get_releaselinks, xom.keyfs.
The mirror access control (package_allowlist / package_denylist) is implemented
on top of devpi-server's stage customizer hooks (get_projects_filter_iter,
get_versions_filter_iter, get_simple_links_filter_iter). devpi-server rejects
duplicate customizer registrations for a given index_type, so instead of providing
our own class we monkey-patch our methods onto the upstream MirrorCustomizer
(an empty pass-through class designed exactly for this kind of extension). The
patch runs once at module import. The tween additionally enforces denylist on
direct +f/ downloads to neutralise previously-cached or shared file URLs.
Requirements
- Python 3.9+
- devpi-server 6.19 <= version < 7.0 - we rely on
tx.get_value_at, theX-DEVPI-REPLICA-UUIDheader and thepolling_replicasdict shape introduced in 6.19; the upper bound is held until 7.x compatibility is verified. - A browser with ES6 support (
Promise,fetch,sessionStorage)
Routes (UI)
Routing is hash-based, so any of these URLs can be bookmarked or shared:
| Hash | View |
|---|---|
# |
Status dashboard (default) |
#indexes |
All indexes |
#indexes/<user> |
Indexes filtered by user |
#packages/<user>/<index> |
Packages in an index |
#package/<user>/<index>/<name> |
Package detail (latest version) |
#package/<user>/<index>/<name>?version=<ver> |
Specific version |
#users |
User management (requires login) |
API
In addition to serving the SPA, devpi-admin exposes its own JSON API under
/+admin-api/. Authentication uses the standard devpi-server header
X-Devpi-Auth: base64(user:token). Responses are application/json unless noted
(/+admin-api/pip-conf returns text/plain).
Session and discovery
GET /+admin-api/session
Cheap auth check; the frontend pings this on tab focus to detect expired sessions.
- Auth: required
- 200:
{"valid": true, "user": "alice"} - 403: not authenticated
GET /+admin-api/public-url
Canonical "outside" URL of this deployment, derived from
request.application_url (respects --outside-url and X-Forwarded-* headers).
The SPA uses this for static pip.conf / .pypirc previews so they match what the
backend would emit when behind a reverse proxy.
- Auth: none (URL is not a secret; even anonymous viewers of public indexes need it)
- 200:
{"url": "https://devpi.example.com"}
Project metadata
GET /+admin-api/versions/{user}/{index}/{project}
All known versions of a project, newest first. Backed by stage.list_versions() so
the result is consistent across primary and replicas (PROJSIMPLELINKS in keyfs is
replicated via the changelog).
- Auth:
pkg_readon the index - 200:
{"versions": ["1.0", "0.9", "0.8"]}
GET /+admin-api/versiondata/{user}/{index}/{project}/{version}
Metadata + file links for a single version (PEP 426 / PEP 621 fields plus +links
with href, basename, hash_spec, upload log).
- Auth:
pkg_readon the index - 200:
{"result": {...}} - 404: version doesn't exist
Tokens
Tokens are opaque adm_<id>.<secret> strings bound to a (user, index, scope) triple.
Only the SHA-256 of the secret is persisted in keyfs.
POST /+admin-api/token
Issue a new token.
- Auth: required (regular user for self; root may issue for other users; admin-token requests cannot issue further tokens)
- Body (JSON):
{ "user": "alice", // optional, default = authenticated; root may set freely (not "root") "index": "alice/dev", // required "scope": "read" | "upload", // required "ttl_seconds": 3600, // optional; 60 <= ttl <= 1 year, default 1h "label": "ci-build", // optional, <= 200 chars "wait_replicas": 10 // optional; block up to N seconds for replicas to catch up }
- 200:
{token, user, index, scope, issued_at, expires_at, label, replication?}-tokenis the plaintext, returned once. - 403: target user lacks scope perm on index, root issuing for itself, admin-token call, etc.
- 404: index doesn't exist
GET /+admin-api/pip-conf?index=u/i&user=&ttl=&label=&wait_replicas=
Issue a read token + return a ready-to-use pip.conf in one call (CI/Ansible-friendly).
- Auth: required (same rules as
POST /token) - 200:
text/plain[global] index-url = https://alice:adm_xxx.yyy@devpi.example.com/alice/dev/+simple/ trusted-host = devpi.example.com
GET /+admin-api/users/{user}/tokens
List active tokens for a user.
- Auth: the user themselves, or root
- 200:
{"result": [{id, id_short, user, index, scope, issuer, issued_at, expires_at, expires_in, label, client_ip}, ...], "count": N}
DELETE /+admin-api/users/{user}/tokens
Revoke ALL tokens for a user.
- Auth: the user themselves, or root
- 200:
{"revoked": N, "user": "alice"}
GET /+admin-api/indexes/{user}/{index}/tokens
List tokens bound to an index. Non-root callers see only tokens they own; root sees
every token for the index. Returns 404 (not 403) when the caller has no pkg_read so
private index existence is not leaked.
- Auth:
pkg_readon the index (404 otherwise) - 200:
{"result": [...], "count": N}- same record shape as/users/{user}/tokens
DELETE /+admin-api/tokens/{token_id}
Revoke a single token.
- Auth: owner of the token, or root
- 200:
{"revoked": true, "id": "abc..."} - 404: token id not found
Replication observability (primary only)
GET /+admin-api/replicas
Last-known poll info per replica, captured from each GET /+changelog/{N}- request via
a tween. The applied_serial field is the highest serial the replica has actually
applied (start_serial - 1 from its most recent poll). Compare against /+status
serial for true lag.
Why this isn't polling_replicas from /+status: devpi-server overwrites
xom.polling_replicas[uuid].serial during streaming and gives a misleading "caught up"
reading once the response generator drains. Capturing start_serial at the request
boundary is the only stable signal the primary alone can produce.
- Auth: required
- 200:
{ "result": { "<replica-uuid>": { "start_serial": 103, "applied_serial": 102, "last_seen": 1712345678.9, "age_seconds": 3, "stuck_seconds": 47, "remote_ip": "10.0.0.5", "outside_url": "https://replica.example.com" } } }
- Entries auto-expire after 10 min of silence. Dict size capped at 256 entries (least-recently-seen evicted first) so an attacker spamming UUIDs cannot exhaust primary memory.
Project layout
devpi-admin/
├── pyproject.toml
├── README.md
├── LICENSE
├── .github/workflows/
│ ├── tests.yml - CI on push/PR (Python 3.10 - 3.14)
│ └── publish.yml - publish to PyPI on release
├── dev/ - untracked dev-only prototypes (e.g. demo-graph.html)
├── devpi_admin/
│ ├── __init__.py - version (from git tag via setuptools-scm)
│ ├── main.py - Pyramid hooks, tween, API views
│ ├── tokens.py - admin token gen / lookup / revoke / list (keyfs storage)
│ ├── customizer.py - mirror package allow/deny filter (patches MirrorCustomizer)
│ └── static/
│ ├── index.html - SPA entry point
│ ├── css/style.css
│ └── js/
│ ├── api.js - devpi REST wrapper + auth
│ ├── theme.js - theme toggle (light/dark/auto)
│ ├── marked.min.js - vendored markdown renderer
│ └── app.js - routing, views, rendering
└── tests/
├── test_acl_read.py - acl_read hooks, tween guards (scope/index), token issuance
│ rules, _check_index_perm, USER-changed handler, replica poll
│ tween + endpoint, public-url
├── test_filter.py - package allow/deny customizer + tween +f/ block
├── test_hooks.py - pluggy hook registration
├── test_json_safe.py - readonly view conversion
├── test_package.py - entry point, static files
├── test_pipconf.py - pip.conf credential helpers
├── test_tokens.py - token format, issue/lookup/revoke, reset_for_index,
│ list_for_index, end-to-end cleanup chain
├── test_tween.py - redirect behavior
├── test_view_helpers.py - _get_stage_or_404, _check_read_access, CSP headers
└── test_wants_html.py - Accept header heuristic
Development
git clone <repo>
cd devpi-admin
python -m venv .venv
.venv/bin/pip install -e ".[dev]"
The dev extra pulls in pytest. A bare pip install -e . works too - the test suite
is also runnable with the stdlib unittest runner.
The static files live at devpi_admin/static/ and can be edited in place - changes show
up on the next browser reload, no restart of devpi-server required (static views read
from disk on each request). Python changes (main.py, tokens.py) require a
devpi-server restart.
Run the unit tests:
# pytest (recommended for local development)
pytest tests/ -q
# unittest (matches the CI invocation)
PYTHONWARNINGS="ignore::UserWarning" python -m unittest discover -v tests/
(The PYTHONWARNINGS shim hides an unrelated deprecation warning emitted by Pyramid 2.1
when it imports pkg_resources.)
Releasing
Version is derived from the git tag via setuptools-scm. To release:
git tag v0.1.0 && git push --tags- On GitHub: Releases -> Draft new release -> select tag -> Publish
- The
publish.ymlworkflow runs tests, builds wheel+sdist, and uploads to PyPI via trusted publishing (no API tokens needed - configure the GitHub environmentpypiin PyPI settings).
Author
Pavel Revak pavelrevak@gmail.com
License
MIT - see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file devpi_admin-1.4.2.tar.gz.
File metadata
- Download URL: devpi_admin-1.4.2.tar.gz
- Upload date:
- Size: 152.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
732bb90087112dfb5c47da7c77734a8b73f0909a6de54fa5fcf300b7617e69b7
|
|
| MD5 |
68a397259842af8c5c6016905a766f64
|
|
| BLAKE2b-256 |
5392b5032eb8694ce0d29f15ebd5e305ed1e53235f74a8d4cff1f74d3d44d4b6
|
Provenance
The following attestation bundles were made for devpi_admin-1.4.2.tar.gz:
Publisher:
publish.yml on pavelrevak/devpi-admin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
devpi_admin-1.4.2.tar.gz -
Subject digest:
732bb90087112dfb5c47da7c77734a8b73f0909a6de54fa5fcf300b7617e69b7 - Sigstore transparency entry: 1522966196
- Sigstore integration time:
-
Permalink:
pavelrevak/devpi-admin@1bf2e00e1c064c9020a86a13b1f7a2affbc5edee -
Branch / Tag:
refs/tags/v1.4.2 - Owner: https://github.com/pavelrevak
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1bf2e00e1c064c9020a86a13b1f7a2affbc5edee -
Trigger Event:
release
-
Statement type:
File details
Details for the file devpi_admin-1.4.2-py3-none-any.whl.
File metadata
- Download URL: devpi_admin-1.4.2-py3-none-any.whl
- Upload date:
- Size: 104.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
39e46d83db5af1a3ed28b60bb908fe60ce57a302d4fca8a5dc731b468e45eccd
|
|
| MD5 |
73dcdd85f499781ae410d08d53a6c62a
|
|
| BLAKE2b-256 |
ee50e48e1369aac4a230f0fc7cfd0a38c167d98c84f46a583ccb109c202913d0
|
Provenance
The following attestation bundles were made for devpi_admin-1.4.2-py3-none-any.whl:
Publisher:
publish.yml on pavelrevak/devpi-admin
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
devpi_admin-1.4.2-py3-none-any.whl -
Subject digest:
39e46d83db5af1a3ed28b60bb908fe60ce57a302d4fca8a5dc731b468e45eccd - Sigstore transparency entry: 1522966225
- Sigstore integration time:
-
Permalink:
pavelrevak/devpi-admin@1bf2e00e1c064c9020a86a13b1f7a2affbc5edee -
Branch / Tag:
refs/tags/v1.4.2 - Owner: https://github.com/pavelrevak
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1bf2e00e1c064c9020a86a13b1f7a2affbc5edee -
Trigger Event:
release
-
Statement type: