relay enrollment could orphan an installation

This commit is contained in:
Prabhaav Pillai
2026-11-09 13:26:00 -05:00
parent 364dbdbe5f
commit 574aa45d99
3 changed files with 20 additions and 4 deletions
@@ -178,7 +178,7 @@ export const SentryNotifications = {
<p v-if="!nativePush" class="gx-note">App alerts can be set up and managed in the Galaxy app.</p>
<template v-else>
<p class="gx-note" role="status">{{ relay ? relayStatus() : notifications.appPushAvailable ? 'Push relay configured.' : 'Configure a push relay to deliver phone alerts.' }}</p>
<div v-if="relay && ['unenrolled', 'revoked', 'actionRequired'].includes(relay.state)" class="gx-actions">
<div v-if="relay && ['unenrolled', 'revoked'].includes(relay.state)" class="gx-actions">
<button type="button" class="gx-btn" :disabled="!relay.automaticConfigured" @click="enableHostedAlerts">Enable alerts</button>
<span class="gx-note">{{ relay.automaticConfigured ? 'Sends phone destinations and alert details to the hosted relay.' : 'Hosted alerts are not configured in this release yet.' }}</span>
</div>
@@ -205,7 +205,7 @@ export const SentryNotifications = {
<div class="gx-stack gx-sentry-relay">
<template v-if="relay">
<dl v-if="relay.installationId" class="gx-details"><dt>Installation ID</dt><dd>{{ relay.installationId }}</dd></dl>
<template v-if="['unenrolled', 'revoked', 'actionRequired'].includes(relay.state)">
<template v-if="['unenrolled', 'revoked'].includes(relay.state)">
<div class="gx-sentry-channel__fields">
<label>Custom relay address <input v-model="relayUrl" class="gx-field" type="url" autocomplete="off" placeholder="https://your-relay.workers.dev"></label>
<label>One-time enrollment code <input v-model="enrollmentCode" class="gx-field" type="password" autocomplete="off"></label>
@@ -213,7 +213,7 @@ export const SentryNotifications = {
</template>
<label><input type="checkbox" v-model="relayConfirmed"> Tick to enable the buttons below. Enrolling shares your phone destinations and alert details with the relay operator. Forget locally does not revoke a copied credential.</label>
<div class="gx-actions">
<button v-if="['unenrolled', 'revoked', 'actionRequired'].includes(relay.state)" type="button" class="gx-btn gx-btn--tonal" :disabled="!relayConfirmed || !relayUrl || !enrollmentCode" @click="enrollRelay">Enroll with custom relay</button>
<button v-if="['unenrolled', 'revoked'].includes(relay.state)" type="button" class="gx-btn gx-btn--tonal" :disabled="!relayConfirmed || !relayUrl || !enrollmentCode" @click="enrollRelay">Enroll with custom relay</button>
<button v-if="relay.state === 'enrolled'" type="button" class="gx-btn gx-btn--tonal" :disabled="!relayConfirmed" @click="relayAction('relayRotate')">Rotate credential</button>
<button v-if="!['unenrolled', 'revoked'].includes(relay.state)" type="button" class="gx-btn gx-btn--tonal" :disabled="!relayConfirmed" @click="relayAction('relayRevoke')">Revoke relay access</button>
<button v-if="relay.state !== 'unenrolled'" type="button" class="gx-btn gx-btn--tonal" :disabled="!relayConfirmed" @click="relayAction('relayForget')">Forget locally</button>
@@ -295,7 +295,7 @@ class NotificationOwner:
current = self.state['relayEnrollment']
if automatic and current['state'] in ('enrolling', 'enrolled', 'rotating', 'confirming'):
return self.snapshot()
if current['state'] not in ('unenrolled', 'revoked', 'actionRequired'):
if current['state'] not in ('unenrolled', 'revoked'):
raise ValueError('Revoke or forget the current enrollment first')
url = DEFAULT_RELAY_URL if automatic else payload['url']
if not url:
@@ -123,6 +123,22 @@ def test_owner_rejects_silent_enrollment_and_host_changes(tmp_path):
owner.close()
def test_owner_requires_resolving_action_required_relay_before_enrollment(tmp_path):
import pytest
from types import SimpleNamespace
from openpilot.starpilot.sentry_mode.notifications import NotificationOwner
owner = NotificationOwner(tmp_path / 'notifications', SimpleNamespace(snapshot=lambda: {'events': []}))
try:
owner.state['relayEnrollment'] = relay.begin('https://relay.example', 'a' * 64)
owner.state['relayEnrollment'].update(state='actionRequired', invitation='')
with pytest.raises(ValueError):
owner.action({'action': 'relayEnroll', 'confirmed': True})
owner.action({'action': 'relayRevoke', 'confirmed': True})
assert owner.state['relayEnrollment']['state'] == 'revoking'
finally:
owner.close()
def test_rotation_recovers_after_old_credential_overlap_expired():
state = relay.begin('https://relay.example', 'a' * 64)
state.update(state='rotating', invitation='', candidate='b' * 64)