Skip to content

Commit e4b1fa0

Browse files
fix(rbac): read KB tuples with valid OpenFGA query
The data_source backfill attempted to use `knowledge_base:` as an OpenFGA tuple-key prefix filter. OpenFGA rejects that shape because the object id is empty, which blocked migration preview/status loading. Read valid pages and filter knowledge_base objects client-side instead. Assisted-by: Cursor:GPT-5.5 Signed-off-by: Sri Aradhyula <sraradhy@cisco.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 01cc9f3 commit e4b1fa0

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

ui/src/lib/rbac/migrations/__tests__/agent-organization-inheritance.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@ jest.mock("@/lib/mongodb", () => ({
22
getCollection: jest.fn(),
33
}));
44

5+
const mockReadOpenFgaTuples = jest.fn();
6+
const mockWriteOpenFgaTuples = jest.fn();
7+
8+
jest.mock("@/lib/rbac/openfga", () => ({
9+
readOpenFgaTuples: (...args: unknown[]) => mockReadOpenFgaTuples(...args),
10+
writeOpenFgaTuples: (...args: unknown[]) => mockWriteOpenFgaTuples(...args),
11+
}));
12+
513
import {
14+
DATA_SOURCE_GRANTS_BACKFILL_MIGRATION_ID,
615
deriveAdminSurfaceRagDatasourcesAdminGrantPlan,
716
deriveAgentOrganizationInheritancePlan,
817
deriveAgentSharedTeamGrantsPlan,
@@ -11,8 +20,13 @@ import {
1120
deriveMcpToolGrantsBackfillPlan,
1221
deriveOrganizationMembershipPlan,
1322
deriveSkillHubTeamGrantPlan,
23+
planMigration,
1424
} from "../registry";
1525

26+
beforeEach(() => {
27+
jest.clearAllMocks();
28+
});
29+
1630
describe("agent organization inheritance migration", () => {
1731
it("plans organization-admin manager tuples for existing dynamic agents", () => {
1832
const plan = deriveAgentOrganizationInheritancePlan([
@@ -380,6 +394,35 @@ describe("knowledge_base shared-team grants migration", () => {
380394
});
381395

382396
describe("data_source grants backfill migration", () => {
397+
it("plans from paginated OpenFGA reads without sending an invalid knowledge_base prefix filter", async () => {
398+
mockReadOpenFgaTuples.mockResolvedValueOnce({
399+
tuples: [
400+
{
401+
key: {
402+
user: "team:platform#member",
403+
relation: "reader",
404+
object: "knowledge_base:kb-alpha",
405+
},
406+
},
407+
{
408+
key: {
409+
user: "team:platform#member",
410+
relation: "reader",
411+
object: "agent:agent-1",
412+
},
413+
},
414+
],
415+
continuationToken: undefined,
416+
});
417+
418+
const plan = await planMigration(DATA_SOURCE_GRANTS_BACKFILL_MIGRATION_ID);
419+
420+
expect(mockReadOpenFgaTuples).toHaveBeenCalledWith({ pageSize: 100 });
421+
expect(plan.tuples).toEqual([
422+
{ user: "team:platform#member", relation: "reader", object: "data_source:kb-alpha" },
423+
]);
424+
});
425+
383426
it("mirrors every knowledge_base tuple as a data_source tuple", () => {
384427
const plan = deriveDataSourceGrantsBackfillPlan([
385428
{ user: "team:platform#member", relation: "reader", object: "knowledge_base:kb-alpha" },

ui/src/lib/rbac/migrations/registry.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1888,11 +1888,11 @@ async function loadKnowledgeBaseSharedTeamGrantsInputs(): Promise<{
18881888
}
18891889

18901890
/**
1891-
* Read every existing `knowledge_base:*` tuple from OpenFGA. Used by
1892-
* `deriveDataSourceGrantsBackfillPlan` so the data_source mirror set
1893-
* is computed from the source of truth instead of Mongo. Iterates
1894-
* the OpenFGA `read` API in pages until `continuationToken` is empty
1895-
* so we don't blow up memory on large stores.
1891+
* Read existing `knowledge_base:*` tuples from OpenFGA. Used by
1892+
* `deriveDataSourceGrantsBackfillPlan` so the data_source mirror set is
1893+
* computed from the source of truth instead of Mongo. OpenFGA does not
1894+
* accept `knowledge_base:` as a tuple-key prefix filter, so this iterates
1895+
* the valid paginated read API and filters object type client-side.
18961896
*
18971897
* Failures (OpenFGA unreachable, model not loaded) bubble up so the
18981898
* migration runner can surface the underlying error rather than
@@ -1903,11 +1903,13 @@ async function loadKnowledgeBaseTuples(): Promise<OpenFgaTupleKey[]> {
19031903
let continuationToken: string | undefined;
19041904
do {
19051905
const page = await readOpenFgaTuples({
1906-
tuple: { user: "", relation: "", object: "knowledge_base:" },
19071906
continuationToken,
1907+
pageSize: 100,
19081908
});
19091909
for (const entry of page.tuples) {
1910-
collected.push(entry.key);
1910+
if (entry.key.object.startsWith("knowledge_base:")) {
1911+
collected.push(entry.key);
1912+
}
19111913
}
19121914
continuationToken = page.continuationToken;
19131915
} while (continuationToken);

0 commit comments

Comments
 (0)