Accept managed collector service tokens on the collector API
All checks were successful
CI / backend (push) Successful in 1m21s
CI / naming (push) Successful in 1s
CI / frontend (push) Successful in 7s

A token scoped to the new collector.ingest permission is a collector
service token: the collector endpoints accept it via X-API-Key or
Bearer alongside the env fleet keys (which remain the fallback), giving
the fleet credential rotation, revocation, and last-used visibility
from the API Tokens page. Containment holds both ways: a collector
token authorizes nothing else, and no other credential gains collector
access. Shared token validation refactored out of the auth shim; a
Collector service token quick-preset in the create modal; integration
guide documents minting, rotation via site-config.json, and the
service-identity pattern.

765 tests pass; live acceptance matrix verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-07-12 09:13:33 -04:00
parent 848a8fb34f
commit 12175169e4
9 changed files with 381 additions and 29 deletions

View File

@@ -125,6 +125,18 @@
<small class="form-hint">Leave blank for a token that never expires.</small>
</div>
<div v-if="canCollectorPreset" class="form-group preset-group">
<button type="button" class="btn btn-secondary btn-sm"
@click="applyCollectorPreset">
Collector service token
</button>
<small class="form-hint">
For GE-Enforce / fleet reporting. Pre-selects only
<code>collector.ingest</code>; the token works only on the
collector API and nothing else.
</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" v-model="form.restrict" />
@@ -308,6 +320,17 @@ const availableGrouped = computed(() => {
return result
})
// The collector preset is offered only when the caller can actually mint a
// collector.ingest token (admins hold everything; others must hold the perm).
const COLLECTOR_SCOPE = 'collector.ingest'
const canCollectorPreset = computed(
() => isAdmin.value || myPermissions.value.includes(COLLECTOR_SCOPE))
function applyCollectorPreset() {
form.value.restrict = true
form.value.scopes = [COLLECTOR_SCOPE]
}
onMounted(() => loadData())
async function loadData() {
@@ -499,6 +522,17 @@ async function revokeToken() {
color: var(--text-light);
margin-top: 0.25rem;
}
.preset-group {
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg);
}
.preset-group code {
background: var(--bg-card);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.secret-warning {
color: var(--danger);
margin-bottom: 1rem;