Skip to content

Commit b74508f

Browse files
Sensitivity label parsing fix in ODSP join session call (microsoft#25186)
## Description Fixes parsing of sensitivity label information in the ODSP join session response. So this was double-parsed, leading to runtime errors when sensitivity label information was actually present. In practice, sensitivity label information hasn't been present, because it wasn't rolled out ODSP side and we didn't sent the `Return-Sensitivity-Labels` Prefer header. We're now setting this header, but have control over it via a feature gate. Once ODSP rollout is done, we can test and roll out on our end, and eventually delete the feature gate again. ## Breaking Changes This breaks the legacy/alpha API of sensitivityLabelsInfo in ISocketStorageDiscovery, switching from string to a more specific object type. At runtime, when sensitivity label information had been present, this would have been an object all along. However, in production this as not been present, and shouldn't have been used by any consumer. So this makes sensitivityLabelsInfo correct and usable for the first time.
1 parent b665ba8 commit b74508f

File tree

9 files changed

+179
-43
lines changed

9 files changed

+179
-43
lines changed

.changeset/dry-frogs-change.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@fluidframework/odsp-driver": minor
3+
"@fluidframework/odsp-driver-definitions": minor
4+
"__section": fix
5+
---
6+
Fixed bug in parsing sensitivity label information from the ODSP join session response
7+
8+
Fixes parsing of sensitivity label information in the ODSP join session response. If there had been sensitivity label data present in the ODSP response, this data would have been double-parsed, leading to runtime errors. This issue was so far not hit in practice, because ODSP did not roll out sensitivity labels in the response yet. This bug fix gets odsp-driver ready for that rollout, which is planned to happen soon.

packages/drivers/odsp-driver-definitions/api-report/odsp-driver-definitions.legacy.alpha.api.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,20 @@ export interface IRelaySessionAwareDriverFactory extends IProvideSessionAwareDri
150150
getRelayServiceSessionInfo(resolvedUrl: IResolvedUrl): Promise<ISocketStorageDiscovery | undefined>;
151151
}
152152

153+
// @alpha @legacy
154+
export interface ISensitivityLabel {
155+
appliedByUserEmail: string;
156+
assignmentMethod: string;
157+
sensitivityLabelId: string;
158+
tenantId: string;
159+
}
160+
161+
// @alpha @legacy
162+
export interface ISensitivityLabelsInfo {
163+
labels: ISensitivityLabel[];
164+
timestamp: string;
165+
}
166+
153167
// @alpha @legacy
154168
export interface ISharingLink extends ISharingLinkKind {
155169
// (undocumented)
@@ -187,7 +201,7 @@ export interface ISocketStorageDiscovery {
187201
refreshSessionDurationSeconds?: number;
188202
// (undocumented)
189203
runtimeTenantId?: string;
190-
sensitivityLabelsInfo?: string;
204+
sensitivityLabelsInfo?: ISensitivityLabelsInfo;
191205
// (undocumented)
192206
snapshotStorageUrl: string;
193207
socketToken?: string;

packages/drivers/odsp-driver-definitions/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@
105105
"typescript": "~5.4.5"
106106
},
107107
"typeValidation": {
108-
"broken": {},
108+
"broken": {
109+
"Interface_ISocketStorageDiscovery": {
110+
"forwardCompat": false,
111+
"backCompat": false
112+
}
113+
},
109114
"entrypoint": "legacy"
110115
}
111116
}

packages/drivers/odsp-driver-definitions/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,7 @@ export {
4545
export {
4646
IProvideSessionAwareDriverFactory,
4747
IRelaySessionAwareDriverFactory,
48+
ISensitivityLabel,
49+
ISensitivityLabelsInfo,
4850
ISocketStorageDiscovery,
4951
} from "./sessionProvider.js";

packages/drivers/odsp-driver-definitions/src/sessionProvider.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,35 @@ export interface ISocketStorageDiscovery {
4545
* response will contain empty labels when the file has no labels, so this field will be there
4646
* even if file has no labels when the service will implement this contract.
4747
*/
48-
sensitivityLabelsInfo?: string;
48+
sensitivityLabelsInfo?: ISensitivityLabelsInfo;
49+
}
50+
51+
/**
52+
* Sensitivity labels information for a file, part of the socket storage discovery response.
53+
* @legacy
54+
* @alpha
55+
*/
56+
export interface ISensitivityLabelsInfo {
57+
/** ISO format timestamp when the label info snapshot was generated. */
58+
timestamp: string;
59+
/** List of applied sensitivity labels. Empty if none. */
60+
labels: ISensitivityLabel[];
61+
}
62+
63+
/**
64+
* A single sensitivity label applied to a document, part of the socket storage discovery response.
65+
* @legacy
66+
* @alpha
67+
*/
68+
export interface ISensitivityLabel {
69+
/** Unique identifier of the sensitivity label. */
70+
sensitivityLabelId: string;
71+
/** Tenant under which the label is defined. */
72+
tenantId: string;
73+
/** How the label was assigned, for example "standard". */
74+
assignmentMethod: string;
75+
/** Email of the user who applied the label. */
76+
appliedByUserEmail: string;
4977
}
5078

5179
/**

packages/drivers/odsp-driver-definitions/src/test/types/validateOdspDriverDefinitionsPrevious.generated.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ declare type current_as_old_for_Interface_ISnapshotOptions = requireAssignableTo
346346
* typeValidation.broken:
347347
* "Interface_ISocketStorageDiscovery": {"forwardCompat": false}
348348
*/
349+
// @ts-expect-error compatibility expected to be broken
349350
declare type old_as_current_for_Interface_ISocketStorageDiscovery = requireAssignableTo<TypeOnly<old.ISocketStorageDiscovery>, TypeOnly<current.ISocketStorageDiscovery>>
350351

351352
/*
@@ -355,6 +356,7 @@ declare type old_as_current_for_Interface_ISocketStorageDiscovery = requireAssig
355356
* typeValidation.broken:
356357
* "Interface_ISocketStorageDiscovery": {"backCompat": false}
357358
*/
359+
// @ts-expect-error compatibility expected to be broken
358360
declare type current_as_old_for_Interface_ISocketStorageDiscovery = requireAssignableTo<TypeOnly<current.ISocketStorageDiscovery>, TypeOnly<old.ISocketStorageDiscovery>>
359361

360362
/*

packages/drivers/odsp-driver/src/odspDelayLoadedDeltaStream.ts

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
type IOdspError,
2626
type IOdspResolvedUrl,
2727
type ISocketStorageDiscovery,
28+
type ISensitivityLabelsInfo,
2829
type InstrumentedStorageTokenFetcher,
2930
OdspErrorTypes,
3031
type TokenFetchOptions,
@@ -189,9 +190,7 @@ export class OdspDelayLoadedDeltaStream {
189190
);
190191
}
191192
if (websocketEndpoint.sensitivityLabelsInfo !== undefined) {
192-
this.emitMetaDataUpdateEvent({
193-
sensitivityLabelsInfo: websocketEndpoint.sensitivityLabelsInfo,
194-
});
193+
this.emitSensitivityLabelUpdateEvent(websocketEndpoint.sensitivityLabelsInfo);
195194
}
196195

197196
const connectionId = uuid();
@@ -283,9 +282,9 @@ export class OdspDelayLoadedDeltaStream {
283282
// Drop error
284283
}
285284
if (envelope?.contents?.type === policyLabelsUpdatesSignalType) {
286-
this.emitMetaDataUpdateEvent({
287-
sensitivityLabelsInfo: JSON.stringify(envelope.contents.content),
288-
});
285+
this.emitSensitivityLabelUpdateEvent(
286+
envelope.contents.content as ISensitivityLabelsInfo,
287+
);
289288
}
290289
}
291290
}
@@ -416,6 +415,9 @@ export class OdspDelayLoadedDeltaStream {
416415
const disableJoinSessionRefresh = this.mc.config.getBoolean(
417416
"Fluid.Driver.Odsp.disableJoinSessionRefresh",
418417
);
418+
const setSensitivityLabelHeader = this.mc.config.getBoolean(
419+
"Fluid.Driver.Odsp.setSensitivityLabelHeader",
420+
);
419421
const executeFetch = async (): Promise<{
420422
entryTime: number;
421423
joinSessionResponse: ISocketStorageDiscovery;
@@ -430,14 +432,13 @@ export class OdspDelayLoadedDeltaStream {
430432
requestSocketToken,
431433
options,
432434
disableJoinSessionRefresh,
435+
setSensitivityLabelHeader,
433436
isRefreshingJoinSession,
434437
displayName,
435438
);
436439
// Emit event only in case it is fetched from the network.
437440
if (joinSessionResponse.sensitivityLabelsInfo !== undefined) {
438-
this.emitMetaDataUpdateEvent({
439-
sensitivityLabelsInfo: joinSessionResponse.sensitivityLabelsInfo,
440-
});
441+
this.emitSensitivityLabelUpdateEvent(joinSessionResponse.sensitivityLabelsInfo);
441442
}
442443
return {
443444
entryTime: Date.now(),
@@ -507,17 +508,15 @@ export class OdspDelayLoadedDeltaStream {
507508
return response.joinSessionResponse;
508509
}
509510

510-
private emitMetaDataUpdateEvent(metadata: Record<string, string>): void {
511-
const label = JSON.parse(metadata.sensitivityLabelsInfo) as {
512-
labels: unknown;
513-
timestamp: number;
514-
};
515-
const time = label.timestamp;
516-
assert(time > 0, 0x8e0 /* time should be positive */);
517-
if (time > this.labelUpdateTimestamp) {
518-
this.labelUpdateTimestamp = time;
511+
private emitSensitivityLabelUpdateEvent(
512+
sensitivityLabelsInfo: ISensitivityLabelsInfo,
513+
): void {
514+
const createdTimestamp = Date.parse(sensitivityLabelsInfo.timestamp);
515+
assert(createdTimestamp > 0, 0x8e0 /* time should be positive */);
516+
if (createdTimestamp > this.labelUpdateTimestamp) {
517+
this.labelUpdateTimestamp = createdTimestamp;
519518
this.metadataUpdateHandler({
520-
sensitivityLabelsInfo: metadata.sensitivityLabelsInfo,
519+
sensitivityLabelsInfo: JSON.stringify(sensitivityLabelsInfo),
521520
});
522521
}
523522
}

packages/drivers/odsp-driver/src/test/socketTests/deltaConnectionUpdateTests.spec.ts

Lines changed: 93 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ import { strict as assert } from "node:assert";
77

88
import type { IClient } from "@fluidframework/driver-definitions";
99
import type { ISignalMessage } from "@fluidframework/driver-definitions/internal";
10-
import type { ISocketStorageDiscovery } from "@fluidframework/odsp-driver-definitions/internal";
10+
import type {
11+
ISensitivityLabel,
12+
ISocketStorageDiscovery,
13+
} from "@fluidframework/odsp-driver-definitions/internal";
1114
import {
1215
type ITelemetryLoggerExt,
1316
MockLogger,
@@ -77,17 +80,26 @@ describe("DeltaConnectionMetadata update tests", () => {
7780
await yieldEventLoop();
7881
}
7982

80-
function addJoinSessionStub(label: string): SinonStub {
81-
joinSessionResponse.sensitivityLabelsInfo = JSON.stringify({
82-
labels: label,
83-
timestamp: Date.now(),
84-
});
83+
function addJoinSessionStub(label: ISensitivityLabel): SinonStub {
84+
joinSessionResponse.sensitivityLabelsInfo = {
85+
timestamp: new Date().toISOString(),
86+
labels: [label],
87+
};
8588
const joinSessionStub = stub(fetchJoinSession, mockify.key).callsFake(
8689
async () => joinSessionResponse,
8790
);
8891
return joinSessionStub;
8992
}
9093

94+
function testSensitivityLabelObjectWithId(id: string): ISensitivityLabel {
95+
return {
96+
sensitivityLabelId: id,
97+
tenantId: "tenantId",
98+
assignmentMethod: "standard",
99+
appliedByUserEmail: "fakeemail@microsoft.com",
100+
};
101+
}
102+
91103
before(async () => {
92104
hashedDocumentId = await getHashedDocumentId(driveId, itemId);
93105
});
@@ -98,7 +110,11 @@ describe("DeltaConnectionMetadata update tests", () => {
98110
async (_options) => "token",
99111
async (_options) => "token",
100112
new LocalPersistentCache(2000),
101-
{ snapshotOptions: { timeout: 2000 } },
113+
{
114+
snapshotOptions: { timeout: 2000 },
115+
// Ensure each test uses its own socket reuse namespace and won’t share sockets.
116+
isolateSocketCache: true,
117+
},
102118
);
103119
const locator: OdspFluidDataStoreLocator = {
104120
driveId,
@@ -134,25 +150,79 @@ describe("DeltaConnectionMetadata update tests", () => {
134150
}
135151
}
136152

137-
it("Delta connection metadata should be updated via Fluid signals and join session response", async () => {
153+
it("Join session response parsing", async () => {
138154
await tickClock(1);
139155
socket = new ClientSocketMock();
140156
let eventRaised = false;
141-
let content: Record<string, string>;
157+
158+
const exampleSensitivityLabelsInfo = `{
159+
"timestamp":"2025-05-28T14:56:21-07:00",
160+
"labels":[
161+
{"sensitivityLabelId":"sensitivityLabelId",
162+
"tenantId":"tenantId",
163+
"assignmentMethod":"standard",
164+
"appliedByUserEmail":"fakeemail@microsoft.com"}
165+
]}`;
166+
167+
const joinSessionResponseString = `
168+
{
169+
"@odata.context":"https://microsoft.sharepoint-df.com/_api/v2.1/$metadata#oneDrive.session",
170+
"deltaStorageUrl":"https://fake/deltaStorageUrl",
171+
"deltaStreamSocketUrl":"https://localhost:3001",
172+
"id":"id",
173+
"refreshSessionDurationSeconds":100,
174+
"runtimeTenantId":"1!1!3",
175+
"snapshotStorageUrl":"https://fake/snapshotStorageUrl",
176+
"socketToken":"",
177+
"sensitivityLabelsInfo": ${exampleSensitivityLabelsInfo}
178+
}`;
179+
180+
const parsedResponse = JSON.parse(joinSessionResponseString) as ISocketStorageDiscovery;
142181

143182
const handler = (metadata: Record<string, string>): void => {
144183
eventRaised = true;
145-
assert.strictEqual(
146-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
147-
JSON.parse(metadata.sensitivityLabelsInfo).labels,
148-
content.labels,
149-
"label via event should match",
184+
assert.deepStrictEqual(
185+
JSON.parse(metadata.sensitivityLabelsInfo),
186+
parsedResponse.sensitivityLabelsInfo,
187+
"sensitivity info via event should match",
150188
);
151189
};
152190

153-
let joinSessionStub = addJoinSessionStub("label1");
191+
const joinSessionStub = stub(fetchJoinSession, mockify.key).callsFake(
192+
async () => parsedResponse,
193+
);
194+
195+
service.on("metadataUpdate", handler);
196+
const connection = (await mockSocket(socket as unknown as Socket, async () =>
197+
service.connectToDeltaStream(client),
198+
)) as OdspDocumentDeltaConnection;
199+
assert(eventRaised, "event1 should have been raised");
200+
service.off("metadataUpdate", handler);
201+
joinSessionStub.restore();
202+
203+
assert(!connection.disposed, "connection should not be disposed");
204+
205+
assert(joinSessionStub.calledOnce, "Should called once on first try");
206+
});
207+
208+
it("Delta connection metadata should be updated via Fluid signals and join session response", async () => {
209+
await tickClock(1);
210+
socket = new ClientSocketMock();
211+
let eventRaised = false;
212+
let content: Record<string, ISensitivityLabel[]>;
213+
214+
const handler = (metadata: Record<string, string>): void => {
215+
eventRaised = true;
216+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
217+
const arg1 = JSON.parse(metadata.sensitivityLabelsInfo).labels;
218+
const arg2: ISensitivityLabel[] | undefined = content.labels;
219+
assert.deepStrictEqual(arg1, arg2, "label via event should match");
220+
};
221+
222+
const label1Object = testSensitivityLabelObjectWithId("label1");
223+
let joinSessionStub = addJoinSessionStub(label1Object);
154224

155-
content = { labels: "label1" };
225+
content = { labels: [label1Object] };
156226
service.on("metadataUpdate", handler);
157227
const connection = (await mockSocket(socket as unknown as Socket, async () =>
158228
service.connectToDeltaStream(client),
@@ -170,8 +240,9 @@ describe("DeltaConnectionMetadata update tests", () => {
170240
// Now change label through signal and listen as event on service.
171241
// eslint-disable-next-line require-atomic-updates
172242
eventRaised = false;
173-
content = { labels: "label2" };
174-
const signalContent1 = { labels: "label2", timestamp: Date.now() };
243+
const label2Object = testSensitivityLabelObjectWithId("label2");
244+
content = { labels: [label2Object] };
245+
const signalContent1 = { labels: [label2Object], timestamp: new Date().toISOString() };
175246
const signalMessage1: ISignalMessage = {
176247
clientId: null,
177248
content: JSON.stringify({
@@ -193,16 +264,18 @@ describe("DeltaConnectionMetadata update tests", () => {
193264
// Now update through join session response
194265
// eslint-disable-next-line require-atomic-updates
195266
eventRaised = false;
196-
content = { labels: "label3" };
267+
const label3Object = testSensitivityLabelObjectWithId("label3");
268+
content = { labels: [label3Object] };
197269
service.on("metadataUpdate", handler);
198-
joinSessionStub = addJoinSessionStub("label3");
270+
joinSessionStub = addJoinSessionStub(label3Object);
199271
await tickClock(70000);
200272
joinSessionStub.restore();
201273
assert(eventRaised, "event3 should have been raised");
202274
service.off("metadataUpdate", handler);
203275
});
204276

205277
afterEach(async () => {
278+
service?.dispose();
206279
clock.reset();
207280
socket?.close();
208281
await epochTracker.removeEntries().catch(() => {});

0 commit comments

Comments
 (0)