docs(usage): state in the consent dialog that the connection only runs outwards
The dialog described what is sent and where it goes, but never said which way the connection runs. That is the part an operator is actually being asked to accept: opening an outbound path to someone else's service. PicPeak sends and never pulls. One place in the service reaches the network, it is a POST, and it requests exactly two paths — /api/envelopes, and /api/participant/lookup only when an operator asks for their own export. No scheduled job contacts the collector; the daily rollup is driven solely by an authenticated admin hitting /activity. There is no route the collector could call, and redirect: 'error' means it cannot even point a request somewhere else. From a reply only the acknowledgement for the packet just sent is read, with every field compared against that packet before it is accepted; the stored copy drops the session token and no read path hands it back to the UI. A requested export is streamed to the operator as a file and never interpreted. The consequence is why it belongs in the consent text and not only in the docs: this channel cannot deliver code, configuration or content into an installation, not even from a collector that has been taken over. It is a security property by design rather than by convention. usageOutboundOnly.test.js guards it by source inspection rather than behaviour, because a behavioural test only proves that today's calls behave. It fails the moment someone adds a second fetch, a poll for messages, a scheduled pull, or a public route touching the usage service — verified by injecting each of those.
This commit is contained in:
@@ -304,3 +304,16 @@ it('returns focus to the control that opened the consent dialog', async () => {
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// A security property, not a nicety: the consent dialog is where an operator
|
||||
// decides whether to open a connection at all, so it has to say which way that
|
||||
// connection runs. UsageService makes exactly two outbound POSTs and reads
|
||||
// nothing but the acknowledgement for the packet it just sent.
|
||||
it('states in the consent dialog that the connection only runs outwards', async () => {
|
||||
mount();
|
||||
fireEvent.click(await screen.findByText('productUsage.review'));
|
||||
const dialog = await screen.findByText('productUsage.consentTitle');
|
||||
expect(dialog).toBeTruthy();
|
||||
await screen.findByText('productUsage.sectionOneWay');
|
||||
await screen.findByText('productUsage.oneWay');
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
type ProductFeedback
|
||||
} from '../../../services/productUsage.service';
|
||||
import {
|
||||
ArrowUpFromLine,
|
||||
Globe,
|
||||
ListChecks,
|
||||
MessageSquare,
|
||||
@@ -31,6 +32,10 @@ const DISCLOSURE: {
|
||||
{ key: 'fields', heading: 'sectionFields', Icon: ListChecks },
|
||||
{ key: 'excluded', heading: 'sectionExcluded', Icon: ShieldOff },
|
||||
{ key: 'transport', heading: 'sectionTransport', Icon: Send },
|
||||
// Directly after transport, because it is a property of the transport and
|
||||
// the reason the transport is shaped this way: the connection only ever
|
||||
// runs outwards, so this cannot become a way to push anything in.
|
||||
{ key: 'oneWay', heading: 'sectionOneWay', Icon: ArrowUpFromLine },
|
||||
{ key: 'visibility', heading: 'sectionVisibility', Icon: Globe },
|
||||
{ key: 'deletion', heading: 'sectionDeletion', Icon: Trash2 },
|
||||
{ key: 'feedbackDisclosure', heading: 'sectionFeedback', Icon: MessageSquare }
|
||||
|
||||
Reference in New Issue
Block a user