Let key-less PCs ask to enroll, and let an admin retry a trust-blocked PC
Found in the Windows VM re-test. - A PC installed without the shared key got 401 from POST /api/endpoints/enroll while the shared key was allowed, so no request was recorded and no admin could approve it. Enroll now never refuses: a valid shared key only decides auto-issue on a clean match; no key or an invalid key creates or updates the one pending request for that hostname and serial. An approved request still issues only to its own nonce. - A PC blocked with "server certificate not trusted by this PC" could never be queued again after trust was fixed, because the blocker comes from its last failed upgrade. Naming the PC in a wave now queues it when trust is its only blocker, and the response lists it under "retried"; "next n" still skips it. The Migration table lets such a PC be selected. - counts.pending on GET /enrollment counts pending requests, including ones for unknown hostnames; counts.pendingcomputers keeps the per-computer figure.
This commit is contained in:
@@ -663,7 +663,7 @@ async function loadEnrollment() {
|
||||
const counts = data.counts || {}
|
||||
const enrolled = counts.enrolled || 0
|
||||
// pending PCs asked but hold no credential yet: still on the shared key.
|
||||
const notenrolled = (counts.notenrolled || 0) + (counts.pending || 0)
|
||||
const notenrolled = (counts.notenrolled || 0) + (counts.pendingcomputers || 0)
|
||||
enrollment.value = {
|
||||
enrolled,
|
||||
notenrolled,
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
<tr v-for="row in migrationRows" :key="row.assetid" class="migration-row">
|
||||
<td v-if="isadmin" class="select-cell">
|
||||
<input
|
||||
v-if="row.state === 'ready'"
|
||||
v-if="isSelectable(row)"
|
||||
v-model="selected"
|
||||
type="checkbox"
|
||||
:value="row.assetid"
|
||||
@@ -135,7 +135,12 @@
|
||||
<td><span class="state-chip" :class="row.state">{{ stateLabel(row.state) }}</span></td>
|
||||
<td>{{ row.agentversion || '-' }}</td>
|
||||
<td>{{ formatDate(row.lastreportedat) }}</td>
|
||||
<td class="blockers">{{ (row.blockers || []).join(', ') || '-' }}</td>
|
||||
<td class="blockers">
|
||||
{{ (row.blockers || []).join(', ') || '-' }}
|
||||
<span v-if="isadmin && isTrustRetryable(row)" class="retry-hint">
|
||||
Retry after fixing trust
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -516,6 +521,20 @@ const enrolledPercent = computed(() => {
|
||||
})
|
||||
|
||||
const readyRows = computed(() => migrationRows.value.filter(row => row.state === 'ready'))
|
||||
|
||||
// The server cannot see a PC's trust store, so this blocker only clears when
|
||||
// an admin queues the PC again after fixing trust. Next N never picks it.
|
||||
const TRUST_BLOCKER = 'server certificate not trusted by this PC'
|
||||
|
||||
function isTrustRetryable(row) {
|
||||
const blockers = row.blockers || []
|
||||
return row.state === 'blocked' && blockers.length > 0
|
||||
&& blockers.every(blocker => blocker === TRUST_BLOCKER)
|
||||
}
|
||||
|
||||
function isSelectable(row) {
|
||||
return row.state === 'ready' || isTrustRetryable(row)
|
||||
}
|
||||
const allReadySelected = computed(() =>
|
||||
readyRows.value.length > 0 && readyRows.value.every(row => selected.value.includes(row.assetid)))
|
||||
|
||||
@@ -541,9 +560,9 @@ async function loadMigration({ quiet = false } = {}) {
|
||||
migrationRows.value = data.computers || []
|
||||
waves.value = data.waves || []
|
||||
migrationTotalPages.value = response.data.meta?.pagination?.totalpages || 1
|
||||
// Drop selections that are no longer ready (queued by a wave, went offline).
|
||||
const readyIds = new Set(readyRows.value.map(row => row.assetid))
|
||||
selected.value = selected.value.filter(id => readyIds.has(id))
|
||||
// Drop selections that are no longer selectable (queued by a wave, went offline).
|
||||
const selectableIds = new Set(migrationRows.value.filter(isSelectable).map(row => row.assetid))
|
||||
selected.value = selected.value.filter(id => selectableIds.has(id))
|
||||
migrationError.value = ''
|
||||
} catch (error) {
|
||||
migrationError.value = apiError(error, 'Could not load migration status')
|
||||
@@ -587,6 +606,11 @@ async function startWave(body, count) {
|
||||
const queued = (data.queued || []).length
|
||||
const skipped = data.skipped || []
|
||||
if (queued) toast.success(`Wave ${data.waveid}: upgrade queued for ${queued} PC(s)`)
|
||||
const retried = data.retried || []
|
||||
if (retried.length) {
|
||||
toast.info(`${retried.length} queued again despite the certificate trust blocker: `
|
||||
+ retried.slice(0, 5).join(', '))
|
||||
}
|
||||
if (skipped.length) {
|
||||
toast.info(`${skipped.length} skipped: `
|
||||
+ skipped.slice(0, 5).map(item => `${item.assetid} (${item.reason})`).join(', '))
|
||||
@@ -1045,6 +1069,12 @@ onBeforeUnmount(() => {
|
||||
[data-theme="dark"] .state-chip.pending,
|
||||
[data-theme="dark"] .state-chip.upgrading { color: var(--warning); }
|
||||
|
||||
.retry-hint {
|
||||
display: block;
|
||||
font-size: 0.8em;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.blockers {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-light);
|
||||
|
||||
@@ -23,7 +23,7 @@ function enrollmentPayload(settings = { sharedkeyallowed: true, autoenroll: true
|
||||
data: {
|
||||
data: {
|
||||
settings,
|
||||
counts: { enrolled: 30, notenrolled: 8, pending: 2 },
|
||||
counts: { enrolled: 30, notenrolled: 8, pendingcomputers: 2, pending: 3 },
|
||||
computers: [],
|
||||
requests: [
|
||||
{ requestid: 1, status: 'pending', reason: 'serial mismatch' },
|
||||
|
||||
@@ -35,7 +35,11 @@ function migrationPayload() {
|
||||
computers: [
|
||||
{ assetid: 11, hostname: 'PC-READY', agentversion: '1.19.2', lastreportedat: null, state: 'ready', blockers: [] },
|
||||
{ assetid: 12, hostname: 'PC-BLOCKED', agentversion: '1.18.0', lastreportedat: null, state: 'blocked',
|
||||
blockers: ['offline', 'no serial on record'] }
|
||||
blockers: ['offline', 'no serial on record'] },
|
||||
{ assetid: 13, hostname: 'PC-TRUST', agentversion: '1.19.2', lastreportedat: null, state: 'blocked',
|
||||
blockers: ['server certificate not trusted by this PC'] },
|
||||
{ assetid: 14, hostname: 'PC-TRUST-OFFLINE', agentversion: '1.19.2', lastreportedat: null, state: 'blocked',
|
||||
blockers: ['offline', 'server certificate not trusted by this PC'] }
|
||||
],
|
||||
waves: [
|
||||
{ waveid: 3, createdat: null, createdby: 'admin', size: 25, queued: 25, enrolled: 10, failed: 1, done: false }
|
||||
@@ -51,7 +55,7 @@ function enrollmentPayload() {
|
||||
data: {
|
||||
data: {
|
||||
settings: { sharedkeyallowed: true, autoenroll: true },
|
||||
counts: { enrolled: 40, notenrolled: 58, pending: 2 },
|
||||
counts: { enrolled: 40, notenrolled: 58, pendingcomputers: 1, pending: 2 },
|
||||
computers: [
|
||||
{ assetid: 21, hostname: 'PC-ENROLLED', serialnumber: 'S1', state: 'enrolled',
|
||||
credentialprefix: 'sdbe_abcdefg', issuedat: null, lastusedat: null, enrolledvia: 'auto',
|
||||
@@ -120,6 +124,29 @@ describe('EnrollmentSettings', () => {
|
||||
expect(rows[1].find('input[type="checkbox"]').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('offers a trust-only blocked PC for retry, not one with another blocker too', async () => {
|
||||
const wrapper = await render()
|
||||
const rows = wrapper.findAll('.migration-row')
|
||||
const trust = rows.find(r => r.text().includes('PC-TRUST') && !r.text().includes('OFFLINE'))
|
||||
const mixed = rows.find(r => r.text().includes('PC-TRUST-OFFLINE'))
|
||||
expect(trust.find('input[type="checkbox"]').exists()).toBe(true)
|
||||
expect(trust.find('.retry-hint').text()).toBe('Retry after fixing trust')
|
||||
expect(mixed.find('input[type="checkbox"]').exists()).toBe(false)
|
||||
expect(mixed.find('.retry-hint').exists()).toBe(false)
|
||||
})
|
||||
|
||||
it('includes a trust-blocked PC in Upgrade selected', async () => {
|
||||
vi.stubGlobal('confirm', vi.fn(() => true))
|
||||
const wrapper = await render()
|
||||
const rows = wrapper.findAll('.migration-row')
|
||||
await rows[0].find('input[type="checkbox"]').setValue(true)
|
||||
const trust = rows.find(r => r.text().includes('PC-TRUST') && !r.text().includes('OFFLINE'))
|
||||
await trust.find('input[type="checkbox"]').setValue(true)
|
||||
await wrapper.find('.upgrade-selected').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/migration/wave', { assetids: [11, 13] })
|
||||
})
|
||||
|
||||
it('posts {"next":25} for Upgrade next 25 after a confirm naming the count', async () => {
|
||||
const confirmSpy = vi.fn(() => true)
|
||||
vi.stubGlobal('confirm', confirmSpy)
|
||||
|
||||
Reference in New Issue
Block a user