geenforce: IP allowlist for client endpoints + admin Settings tab
Fleet PCs on a trusted (vaulted) network can now reach the GE-Enforce client endpoints (manifest, payload, report) without a per-PC token: the auth path accepts a valid geenforce.fetch/report token OR a source IP in the configured allowlist (setting geenforce_allowed_cidrs). Fail-closed; an empty allowlist means the token stays the only path, so existing deployments are unchanged. Rationale: the client token lives in HKLM on every kiosk, so it does not defend against a compromised kiosk anyway - network-perimeter trust is the same practical strength with far less provisioning + no token-rotation churn on a DB wipe. Documented in-UI that this is perimeter trust, not per-device identity. - _ip_allowlisted() (ipaddress, X-Forwarded-For-aware via _client_ip) - /geenforce/config GET/PUT extended with allowedcidrs, server-validated + normalized (bad CIDR -> 400) - new GE-Enforce > Settings tab (GeEnforceSettings.vue) to edit the allowlist in admin, no SQL - 3 regression tests (allow by IP, reject outside list, empty = token required)
This commit is contained in:
109
plugins/geenforce/frontend/views/GeEnforceSettings.vue
Normal file
109
plugins/geenforce/frontend/views/GeEnforceSettings.vue
Normal file
@@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<div class="geenforce-settings">
|
||||
<h2>Client Access</h2>
|
||||
<p class="muted">
|
||||
Fleet PCs on a trusted network can reach the GE-Enforce client endpoints
|
||||
(manifest, payload, report) without a per-PC token. List the trusted
|
||||
networks below. Leave it empty to require a <code>geenforce.fetch</code>
|
||||
token instead.
|
||||
</p>
|
||||
|
||||
<label class="field-label" for="cidrs">Allowed networks (CIDR)</label>
|
||||
<textarea
|
||||
id="cidrs"
|
||||
v-model="cidrs"
|
||||
class="cidr-input"
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
placeholder="10.134.48.0/23 10.48.249.0/26"
|
||||
></textarea>
|
||||
<small class="input-hint">
|
||||
One CIDR (or a plain IP) per line, or comma-separated. Example:
|
||||
<code>10.134.48.0/23, 10.48.249.0/26</code>. A caller from any of these
|
||||
networks may pull manifests + payloads and post reports with no token.
|
||||
</small>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" :disabled="saving" @click="save">
|
||||
{{ saving ? 'Saving...' : 'Save' }}
|
||||
</button>
|
||||
<span v-if="message" :class="['status', ok ? 'ok' : 'err']">{{ message }}</span>
|
||||
</div>
|
||||
|
||||
<div class="note">
|
||||
<strong>Note:</strong> this is network-perimeter trust. Any host on a
|
||||
listed network is trusted - including a compromised one. It does not
|
||||
replace per-device identity if you need tamper-proof report integrity.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '@/api'
|
||||
|
||||
const cidrs = ref('')
|
||||
const saving = ref(false)
|
||||
const message = ref('')
|
||||
const ok = ref(false)
|
||||
|
||||
function unwrap(response) { return response.data.data }
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const config = unwrap(await api.get('/geenforce/config'))
|
||||
cidrs.value = config.allowedcidrs || ''
|
||||
} catch (e) {
|
||||
message.value = 'Could not load config.'
|
||||
ok.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
message.value = ''
|
||||
try {
|
||||
const config = unwrap(await api.put('/geenforce/config', { allowedcidrs: cidrs.value }))
|
||||
cidrs.value = config.allowedcidrs || '' // server returns the normalized list
|
||||
ok.value = true
|
||||
message.value = 'Saved.'
|
||||
} catch (e) {
|
||||
ok.value = false
|
||||
message.value = e?.response?.data?.data?.error?.message || 'Save failed.'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.geenforce-settings { max-width: 640px; }
|
||||
.muted { color: var(--text-light); }
|
||||
.field-label { display: block; font-weight: 600; margin: 1rem 0 0.35rem; }
|
||||
.cidr-input {
|
||||
width: 100%;
|
||||
font-family: 'SF Mono', 'Consolas', monospace;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.6rem 0.7rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
background: var(--bg-card-solid);
|
||||
color: var(--text);
|
||||
resize: vertical;
|
||||
}
|
||||
.input-hint { display: block; color: var(--text-light); margin-top: 0.35rem; }
|
||||
.actions { display: flex; align-items: center; gap: 0.75rem; margin-top: 1rem; }
|
||||
.status.ok { color: var(--success); font-weight: 600; }
|
||||
.status.err { color: var(--danger); font-weight: 600; }
|
||||
.note {
|
||||
margin-top: 1.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-left: 4px solid var(--warning);
|
||||
background: var(--bg-card);
|
||||
border-radius: 0.25rem;
|
||||
color: var(--text-light);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user