Make rollback, first upgrades and pin changes work as tested on Windows
Found in an end-to-end test on a Windows 11 VM against a self-signed server.
- Rollback: a 1.20 agent re-enrolls within a second of an un-enroll, so an
older agent pushed afterwards arrived on an enrolled PC and was refused.
Revoke now takes {"blockreenroll": true, "reason": ...}; a blocked PC is
never issued a credential and keeps working on the shared key until
POST .../assets/<assetid>/allow-reenroll. The Enrollment page offers the
block when un-enrolling and shows blocked PCs with an allow button.
- First upgrade of old agents: agents before 1.20 check the server
certificate when downloading the upgrade, so a store wave failed on PCs that
do not trust a self-signed server while the Migration tab said ready. A
failed download now marks the PC blocked with "server certificate not
trusted by this PC". New Install-ShopDBServerCertificate.ps1 (GE-Enforce,
SYSTEM) adds a non-CA server certificate to the trusted roots after checking
its thumbprint; the server certificate page offers a .cer download and the
SHA-1 thumbprint.
- The generated installer writes ServerPins (with an optional next pin for a
planned rotation), can leave the shared key out so the PC waits for
approval, never touches an existing credential, and skips the trusted-root
import for a CA certificate.
- The agent re-reads server pins about every 60 seconds and recycles pooled
connections within 2 minutes, so a pin change needs no restart.
- An alert can only be acknowledged by a PC it targets.
- A wave no longer counts a PC as failed after a later wave upgraded it.
- The scripts report a pin mismatch only when the pin really is missing, and
otherwise log the real error.
- Runbook: trust step before the first wave, rollback order, pin re-read,
verifying the pin against the live URL; agent contract updated.
This commit is contained in:
@@ -54,7 +54,11 @@ function enrollmentPayload() {
|
||||
counts: { enrolled: 40, notenrolled: 58, pending: 2 },
|
||||
computers: [
|
||||
{ assetid: 21, hostname: 'PC-ENROLLED', serialnumber: 'S1', state: 'enrolled',
|
||||
credentialprefix: 'sdbe_abcdefg', issuedat: null, lastusedat: null, enrolledvia: 'auto' }
|
||||
credentialprefix: 'sdbe_abcdefg', issuedat: null, lastusedat: null, enrolledvia: 'auto',
|
||||
reenrollblocked: false, reenrollblockedreason: null },
|
||||
{ assetid: 22, hostname: 'PC-ROLLEDBACK', serialnumber: 'S2', state: 'revoked',
|
||||
credentialprefix: 'sdbe_hijklmn', issuedat: null, lastusedat: null, enrolledvia: 'auto',
|
||||
reenrollblocked: true, reenrollblockedreason: 'rollback' }
|
||||
],
|
||||
requests: [
|
||||
{ requestid: 45, hostname: 'PC-NEW', serialnumber: 'S9', deviceid: null, reason: 'serial mismatch',
|
||||
@@ -125,6 +129,8 @@ describe('EnrollmentSettings', () => {
|
||||
expect(confirmSpy).toHaveBeenCalledTimes(1)
|
||||
expect(confirmSpy.mock.calls[0][0]).toMatch(/Upgrade 25 PC\(s\) to endpoint agent 1\.20\.0/)
|
||||
expect(confirmSpy.mock.calls[0][0]).toMatch(/enrolls automatically/)
|
||||
expect(confirmSpy.mock.calls[0][0]).toMatch(
|
||||
/older than 1\.20 must trust the server certificate before they can download the upgrade/)
|
||||
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/migration/wave', { next: 25 })
|
||||
})
|
||||
|
||||
@@ -166,14 +172,46 @@ describe('EnrollmentSettings', () => {
|
||||
})
|
||||
|
||||
it('un-enrolls through the revoke route', async () => {
|
||||
vi.stubGlobal('confirm', vi.fn(() => true))
|
||||
const wrapper = await render()
|
||||
await openCredentials(wrapper)
|
||||
await wrapper.find('.revoke').trigger('click')
|
||||
expect(api.post).not.toHaveBeenCalled()
|
||||
expect(wrapper.find('.revoke-row').text()).toContain('Keep this PC from enrolling again')
|
||||
expect(wrapper.find('.revoke-block').element.checked).toBe(false)
|
||||
await wrapper.find('.revoke-confirm').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/assets/21/revoke')
|
||||
})
|
||||
|
||||
it('sends blockreenroll with the reason when the block box is ticked', async () => {
|
||||
const wrapper = await render()
|
||||
await openCredentials(wrapper)
|
||||
await wrapper.find('.revoke').trigger('click')
|
||||
await wrapper.find('.revoke-block').setValue(true)
|
||||
await wrapper.find('.revoke-reason').setValue(' rollback ')
|
||||
await wrapper.find('.revoke-confirm').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/assets/21/revoke',
|
||||
{ blockreenroll: true, reason: 'rollback' })
|
||||
})
|
||||
|
||||
it('shows the block chip and allows re-enrollment after confirm', async () => {
|
||||
const confirmSpy = vi.fn(() => true)
|
||||
vi.stubGlobal('confirm', confirmSpy)
|
||||
const wrapper = await render()
|
||||
await openCredentials(wrapper)
|
||||
const rows = wrapper.findAll('.credential-row')
|
||||
const blocked = rows.find(r => r.text().includes('PC-ROLLEDBACK'))
|
||||
const enrolled = rows.find(r => r.text().includes('PC-ENROLLED'))
|
||||
expect(blocked.find('.block-chip').text()).toBe('Re-enroll blocked')
|
||||
expect(enrolled.find('.block-chip').exists()).toBe(false)
|
||||
expect(enrolled.find('.allow-reenroll').exists()).toBe(false)
|
||||
await blocked.find('.allow-reenroll').trigger('click')
|
||||
await flushPromises()
|
||||
expect(confirmSpy).toHaveBeenCalledTimes(1)
|
||||
expect(api.post).toHaveBeenCalledWith('/endpoints/enrollment/assets/22/allow-reenroll')
|
||||
})
|
||||
|
||||
it('shows the not-enrolled count before turning the shared key off', async () => {
|
||||
const promptSpy = vi.fn(() => 'OFF')
|
||||
vi.stubGlobal('prompt', promptSpy)
|
||||
|
||||
Reference in New Issue
Block a user