- Endpoints (shared key) page: the key is presented as what PCs use until they enroll, with enrollment progress, a link to Enrollment, rewritten guidance, and rotate/revoke confirmations that say how many PCs are not enrolled yet. - Settings cards describe the server certificate as what PCs actually receive, on IIS or Docker; Enrollment has its own icon (the icon set lacked the one the Server Certificate card already asked for). - Server Certificate page checks the certificate the site presents at its public address and compares its pin with the certificate file, warning when they differ. The host comes from site settings only; the check is a TLS handshake, never an HTTP request. - Fleet list: Credential column and filter (enrolled, pending, re-enroll blocked, shared key, not reporting), computed in the list query. - PC detail: credential state, prefix and dates for fleet readers, state only for others, with admin Revoke (optionally blocking re-enrollment) and Allow re-enrollment. - Dashboard: pending enrollment requests, and PCs blocked from re-enrolling for more than seven days. - Runbook: where to find each of these in the app.
99 lines
3.5 KiB
JavaScript
99 lines
3.5 KiB
JavaScript
/**
|
|
* Server Certificate live check.
|
|
*
|
|
* A PC pins the certificate the site presents, which a proxy or IIS binding can
|
|
* make different from the file this page manages. The page must show the
|
|
* presented pin, warn plainly when it differs, and let the admin recheck.
|
|
*/
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { mount, flushPromises } from '@vue/test-utils'
|
|
|
|
vi.mock('@/api', () => ({
|
|
default: { get: vi.fn(), post: vi.fn() }
|
|
}))
|
|
|
|
import api from '@/api'
|
|
import ServerCertSettings from './ServerCertSettings.vue'
|
|
|
|
const FILEPIN = 'a'.repeat(64)
|
|
const OTHERPIN = 'b'.repeat(64)
|
|
|
|
function certificate() {
|
|
return {
|
|
data: {
|
|
status: 'success',
|
|
data: {
|
|
subject_cn: 'shopdb.example.test', issuer: 'shopdb.example.test',
|
|
not_before: null, not_after: null, thumbprint: 'AA:BB',
|
|
spkisha256: FILEPIN, sha1thumbprint: 'CC', san_list: [],
|
|
expires_in_days: 300, is_self_signed: true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function render(live) {
|
|
api.get.mockImplementation((path) => Promise.resolve(
|
|
path === '/endpoints/settings/server-cert/live'
|
|
? { data: { status: 'success', data: live } }
|
|
: certificate()))
|
|
const wrapper = mount(ServerCertSettings)
|
|
await flushPromises()
|
|
return wrapper
|
|
}
|
|
|
|
describe('ServerCertSettings live check', () => {
|
|
beforeEach(() => {
|
|
api.get.mockReset()
|
|
})
|
|
|
|
it('shows the presented pin in green when it matches the file', async () => {
|
|
const wrapper = await render({
|
|
configured: true, url: 'https://shopdb.example.test', presentedspkisha256: FILEPIN,
|
|
presentedsha1thumbprint: 'CC', matchesfile: true, error: null
|
|
})
|
|
const check = wrapper.find('.live-check')
|
|
expect(check.find('.live-match').text()).toContain(FILEPIN)
|
|
expect(check.text()).toContain('Matches the pin of the certificate file above')
|
|
expect(check.find('.live-warning').exists()).toBe(false)
|
|
})
|
|
|
|
it('warns in red with both pins when the site presents another certificate', async () => {
|
|
const wrapper = await render({
|
|
configured: true, url: 'https://shopdb.example.test', presentedspkisha256: OTHERPIN,
|
|
presentedsha1thumbprint: 'DD', matchesfile: false, error: null
|
|
})
|
|
const warning = wrapper.find('.live-warning')
|
|
expect(warning.exists()).toBe(true)
|
|
expect(warning.text()).toContain(
|
|
'PCs will be pinned to the certificate they actually receive; use the presented pin')
|
|
expect(warning.text()).toContain(FILEPIN)
|
|
expect(warning.text()).toContain(OTHERPIN)
|
|
expect(wrapper.find('.live-mismatch').exists()).toBe(true)
|
|
})
|
|
|
|
it('says when no site address is configured', async () => {
|
|
const wrapper = await render({ configured: false })
|
|
expect(wrapper.find('.live-unconfigured').exists()).toBe(true)
|
|
})
|
|
|
|
it('shows the check error', async () => {
|
|
const wrapper = await render({
|
|
configured: true, url: 'https://shopdb.example.test', presentedspkisha256: null,
|
|
presentedsha1thumbprint: null, matchesfile: false, error: 'Could not read the certificate'
|
|
})
|
|
expect(wrapper.find('.live-error').text()).toContain('Could not read the certificate')
|
|
})
|
|
|
|
it('rechecks on demand', async () => {
|
|
const wrapper = await render({ configured: false })
|
|
const before = api.get.mock.calls.filter(
|
|
([path]) => path === '/endpoints/settings/server-cert/live').length
|
|
await wrapper.find('.live-recheck').trigger('click')
|
|
await flushPromises()
|
|
const after = api.get.mock.calls.filter(
|
|
([path]) => path === '/endpoints/settings/server-cert/live').length
|
|
expect(after).toBe(before + 1)
|
|
})
|
|
})
|