devpi-admin 1.4.1__tar.gz → 1.4.2__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.4.1 → devpi_admin-1.4.2}/.github/workflows/publish.yml +2 -2
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/.github/workflows/tests.yml +2 -2
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/PKG-INFO +1 -1
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/_version.py +3 -3
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/css/style.css +5 -1
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/js/app.js +73 -9
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/PKG-INFO +1 -1
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_devpi_tokens_ui.py +17 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/.gitignore +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/LICENSE +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/README.md +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/__init__.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/customizer.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/main.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/favicon.svg +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/index.html +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/js/api.js +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/js/marked.min.js +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/static/js/theme.js +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin/tokens.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/SOURCES.txt +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/dependency_links.txt +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/entry_points.txt +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/requires.txt +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/devpi_admin.egg-info/top_level.txt +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/pyproject.toml +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/setup.cfg +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/__init__.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_acl_read.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_filter.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_hooks.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_json_safe.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_package.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_pipconf.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_tokens.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_tween.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_view_helpers.py +0 -0
- {devpi_admin-1.4.1 → devpi_admin-1.4.2}/tests/test_wants_html.py +0 -0
|
@@ -12,12 +12,12 @@ jobs:
|
|
|
12
12
|
id-token: write
|
|
13
13
|
|
|
14
14
|
steps:
|
|
15
|
-
- uses: actions/checkout@
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
16
|
with:
|
|
17
17
|
fetch-depth: 0 # needed for setuptools-scm to derive version from tag
|
|
18
18
|
|
|
19
19
|
- name: Set up Python
|
|
20
|
-
uses: actions/setup-python@
|
|
20
|
+
uses: actions/setup-python@v6
|
|
21
21
|
with:
|
|
22
22
|
python-version: "3.14"
|
|
23
23
|
|
|
@@ -14,12 +14,12 @@ jobs:
|
|
|
14
14
|
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
15
|
|
|
16
16
|
steps:
|
|
17
|
-
- uses: actions/checkout@
|
|
17
|
+
- uses: actions/checkout@v5
|
|
18
18
|
with:
|
|
19
19
|
fetch-depth: 0 # needed for setuptools-scm to read git tags
|
|
20
20
|
|
|
21
21
|
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
-
uses: actions/setup-python@
|
|
22
|
+
uses: actions/setup-python@v6
|
|
23
23
|
with:
|
|
24
24
|
python-version: ${{ matrix.python-version }}
|
|
25
25
|
|
|
@@ -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.4.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 4,
|
|
21
|
+
__version__ = version = '1.4.2'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 4, 2)
|
|
23
23
|
|
|
24
|
-
__commit_id__ = commit_id = '
|
|
24
|
+
__commit_id__ = commit_id = 'g1bf2e00e1'
|
|
@@ -1588,15 +1588,18 @@ body {
|
|
|
1588
1588
|
align-items: center;
|
|
1589
1589
|
justify-content: space-between;
|
|
1590
1590
|
padding: 2px 0;
|
|
1591
|
+
gap: 4px;
|
|
1591
1592
|
}
|
|
1592
1593
|
|
|
1593
1594
|
.pkg-version-link {
|
|
1594
|
-
flex: 1;
|
|
1595
|
+
flex: 1 1 auto;
|
|
1596
|
+
min-width: 0;
|
|
1595
1597
|
padding: 5px 8px;
|
|
1596
1598
|
border-radius: 4px;
|
|
1597
1599
|
font-size: 13px;
|
|
1598
1600
|
color: var(--text-muted);
|
|
1599
1601
|
text-decoration: none;
|
|
1602
|
+
overflow-wrap: anywhere;
|
|
1600
1603
|
}
|
|
1601
1604
|
|
|
1602
1605
|
.pkg-version-link:hover {
|
|
@@ -1619,6 +1622,7 @@ body {
|
|
|
1619
1622
|
font-size: 16px;
|
|
1620
1623
|
line-height: 1;
|
|
1621
1624
|
border-radius: 4px;
|
|
1625
|
+
flex-shrink: 0;
|
|
1622
1626
|
}
|
|
1623
1627
|
|
|
1624
1628
|
.pkg-version-del:hover {
|
|
@@ -1242,10 +1242,65 @@
|
|
|
1242
1242
|
// Token types supported by the unified Issue modal. The selector at the
|
|
1243
1243
|
// top of the modal switches the form between Devpi tokens (multi-index,
|
|
1244
1244
|
// permission checkboxes) and Admin tokens (single index, scope select).
|
|
1245
|
-
//
|
|
1245
|
+
// When the caller doesn't pin a type, the modal defaults to whichever
|
|
1246
|
+
// backend the user *most recently* issued against — saves picking the
|
|
1247
|
+
// same radio every time. Falls back to Devpi (or Admin if the plugin
|
|
1248
|
+
// isn't installed) for users with no tokens yet.
|
|
1246
1249
|
var TOKEN_TYPE_DEVPI = 'devpi';
|
|
1247
1250
|
var TOKEN_TYPE_ADMIN = 'admin';
|
|
1248
1251
|
|
|
1252
|
+
function _detectRecentTokenType(username) {
|
|
1253
|
+
// Returns Promise<TOKEN_TYPE_*|null>. ``null`` means the user has
|
|
1254
|
+
// no tokens (or detection failed) — caller picks a static default.
|
|
1255
|
+
//
|
|
1256
|
+
// Admin tokens carry ``issued_at`` (epoch). Macaroon tokens have
|
|
1257
|
+
// no first-class issuance timestamp; we use ``not_before`` when
|
|
1258
|
+
// present (it tracks the issue moment for the typical "starts
|
|
1259
|
+
// now" flow) and fall back to ``expires`` so a freshly-issued
|
|
1260
|
+
// long-TTL token still wins over an older short-TTL one.
|
|
1261
|
+
var pAdmin = Api.get(
|
|
1262
|
+
'/+admin-api/users/' + encodeURIComponent(username) + '/tokens')
|
|
1263
|
+
.then(function (data) {
|
|
1264
|
+
var tokens = (data && data.result) || [];
|
|
1265
|
+
var max = 0;
|
|
1266
|
+
for (var i = 0; i < tokens.length; i++) {
|
|
1267
|
+
var t = tokens[i].issued_at || 0;
|
|
1268
|
+
if (t > max) max = t;
|
|
1269
|
+
}
|
|
1270
|
+
return max || null;
|
|
1271
|
+
})
|
|
1272
|
+
.catch(function () { return null; });
|
|
1273
|
+
|
|
1274
|
+
var pDevpi = hasDevpiTokens()
|
|
1275
|
+
? Api.get('/' + encodeURIComponent(username) + '/+tokens')
|
|
1276
|
+
.then(function (data) {
|
|
1277
|
+
var tokens = (data && data.result
|
|
1278
|
+
&& data.result.tokens) || {};
|
|
1279
|
+
var max = 0;
|
|
1280
|
+
for (var id in tokens) {
|
|
1281
|
+
if (!Object.prototype.hasOwnProperty.call(
|
|
1282
|
+
tokens, id)) continue;
|
|
1283
|
+
var parsed = parseMacaroonRestrictions(
|
|
1284
|
+
tokens[id].restrictions);
|
|
1285
|
+
var t = parsed.not_before
|
|
1286
|
+
|| parsed.expires || 0;
|
|
1287
|
+
if (t > max) max = t;
|
|
1288
|
+
}
|
|
1289
|
+
return max || null;
|
|
1290
|
+
})
|
|
1291
|
+
.catch(function () { return null; })
|
|
1292
|
+
: Promise.resolve(null);
|
|
1293
|
+
|
|
1294
|
+
return Promise.all([pAdmin, pDevpi]).then(function (results) {
|
|
1295
|
+
var adminTs = results[0];
|
|
1296
|
+
var devpiTs = results[1];
|
|
1297
|
+
if (!adminTs && !devpiTs) return null;
|
|
1298
|
+
if (!adminTs) return TOKEN_TYPE_DEVPI;
|
|
1299
|
+
if (!devpiTs) return TOKEN_TYPE_ADMIN;
|
|
1300
|
+
return adminTs >= devpiTs ? TOKEN_TYPE_ADMIN : TOKEN_TYPE_DEVPI;
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1249
1304
|
// Public entry point. `options` may include:
|
|
1250
1305
|
// • `preselectType` — 'devpi' or 'admin'
|
|
1251
1306
|
// • `preselectIndexes` — list of 'user/index' strings
|
|
@@ -1259,18 +1314,25 @@
|
|
|
1259
1314
|
function showIssueTokenModal(username, options) {
|
|
1260
1315
|
options = options || {};
|
|
1261
1316
|
var presel = (options.preselectIndexes || []).slice();
|
|
1262
|
-
var
|
|
1317
|
+
var explicitType = options.preselectType || null;
|
|
1263
1318
|
_issueReturnTo = options.returnTo || function () {
|
|
1264
1319
|
showTokensModal(username);
|
|
1265
1320
|
};
|
|
1266
1321
|
_issueLockedIndex = (options.lockIndex && presel.length)
|
|
1267
1322
|
? presel[0] : null;
|
|
1268
|
-
//
|
|
1269
|
-
//
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1323
|
+
// Caller pinned the type → honour it; otherwise detect from the
|
|
1324
|
+
// user's existing tokens (most-recently-issued backend wins).
|
|
1325
|
+
// Detection failure falls back to Devpi (or Admin if the plugin
|
|
1326
|
+
// isn't installed).
|
|
1327
|
+
var pType = explicitType
|
|
1328
|
+
? Promise.resolve(explicitType)
|
|
1329
|
+
: _detectRecentTokenType(username);
|
|
1330
|
+
Promise.all([fetchRoot(), pType]).then(function (results) {
|
|
1331
|
+
var rootResult = results[0];
|
|
1332
|
+
var preselType = results[1] || TOKEN_TYPE_DEVPI;
|
|
1333
|
+
if (preselType === TOKEN_TYPE_DEVPI && !hasDevpiTokens()) {
|
|
1334
|
+
preselType = TOKEN_TYPE_ADMIN;
|
|
1335
|
+
}
|
|
1274
1336
|
var indexInfos = getAllIndexes(rootResult);
|
|
1275
1337
|
var aclByIndex = {};
|
|
1276
1338
|
// Indexes accessible to the bound user: ones they own, ones
|
|
@@ -1304,11 +1366,13 @@
|
|
|
1304
1366
|
};
|
|
1305
1367
|
_renderIssueTokenModal(username, presel, preselType);
|
|
1306
1368
|
}).catch(function () {
|
|
1369
|
+
var fallbackType = explicitType
|
|
1370
|
+
|| (hasDevpiTokens() ? TOKEN_TYPE_DEVPI : TOKEN_TYPE_ADMIN);
|
|
1307
1371
|
_issueContext = {
|
|
1308
1372
|
accessibleIndexes: presel.slice(),
|
|
1309
1373
|
aclByIndex: {},
|
|
1310
1374
|
};
|
|
1311
|
-
_renderIssueTokenModal(username, presel,
|
|
1375
|
+
_renderIssueTokenModal(username, presel, fallbackType);
|
|
1312
1376
|
});
|
|
1313
1377
|
}
|
|
1314
1378
|
|
|
@@ -277,6 +277,23 @@ class UnifiedIssueModalTests(unittest.TestCase):
|
|
|
277
277
|
def test_unified_entry_point_exists(self):
|
|
278
278
|
self.assertIn("function showIssueTokenModal(username, options)", self.js)
|
|
279
279
|
|
|
280
|
+
def test_recent_token_type_detector_present(self):
|
|
281
|
+
# Auto-preselect: when caller doesn't pin a type, pick the
|
|
282
|
+
# backend the user most recently issued against.
|
|
283
|
+
self.assertIn("function _detectRecentTokenType(username)", self.js)
|
|
284
|
+
# Admin tokens compared by issued_at, macaroons by
|
|
285
|
+
# not_before / expires.
|
|
286
|
+
self.assertIn("tokens[i].issued_at", self.js)
|
|
287
|
+
self.assertIn("parsed.not_before", self.js)
|
|
288
|
+
|
|
289
|
+
def test_issue_modal_uses_detector_when_caller_is_silent(self):
|
|
290
|
+
# `options.preselectType` short-circuits detection; absence
|
|
291
|
+
# triggers _detectRecentTokenType.
|
|
292
|
+
self.assertIn("var explicitType = options.preselectType || null;",
|
|
293
|
+
self.js)
|
|
294
|
+
self.assertIn("explicitType\n ? Promise.resolve(explicitType)\n"
|
|
295
|
+
" : _detectRecentTokenType(username);", self.js)
|
|
296
|
+
|
|
280
297
|
def test_unified_listing_has_issue_button(self):
|
|
281
298
|
# Single Issue entry point on the unified per-user Tokens modal —
|
|
282
299
|
# no preselectType so the user picks devpi/admin in the form.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|