mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
fix(knowledge-fs): honor capability grants in fs handlers
This commit is contained in:
parent
28b3306867
commit
c944ffe558
@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 5,
|
||||
"subtreeTree": "ae826da4bed6a57241dbed9da2faef4594a2dea5",
|
||||
"subtreeTree": "6be868e420fae6cc28cd832d21eccd505eedbc46",
|
||||
"openapiSha256": "189c98cd2535829d75b090a70c22dac720a9e9349b8b2cde1602abc31d14f8b8",
|
||||
"capabilityV2AuthManifestSha256": "fc0a47e23cce12544882f0298522b4933002e892b84ce1815df7e81d36a7a0c7",
|
||||
"capabilityV2AuthTestVectorSha256": "ae0de37b1ff05c40f905cf17a7b410d8971acacf64db07d5ee3d6fecfa559ce3",
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
# KnowledgeFS Capability v2 CLI authorization
|
||||
|
||||
## What changed
|
||||
|
||||
- Passed the authenticated Capability v2 grant into candidate-content authorization for all
|
||||
KnowledgeFS command handlers: `ls`, `tree`, `grep`, `find`, `diff`, `open_node`, `cat`, `stat`,
|
||||
`write`, and `append`.
|
||||
- Added a handler regression covering Capability v2 requests without the legacy authorization
|
||||
decision and verifying that the grant's normalized content scope reaches every command.
|
||||
|
||||
## Why
|
||||
|
||||
The Capability v2 gateway had already authenticated and authorized CLI requests, but the command
|
||||
handlers only consulted the legacy authorization decision. Capability v2 requests therefore lost
|
||||
their candidate scope at the handler boundary and were rejected with `403 Knowledge space access
|
||||
denied`; Dify surfaced that product response as `503 knowledge_fs_unavailable` for every CLI
|
||||
filesystem command.
|
||||
|
||||
## Performance and safety
|
||||
|
||||
- The fix only forwards the sanitized grant already stored on the request context; it adds no
|
||||
database, model, or network calls.
|
||||
- `currentCandidateGrants` continues to bind the grant to the exact tenant, subject, and knowledge
|
||||
space and to fail closed for malformed or mismatched scopes.
|
||||
- Legacy authorization decisions remain supported as the fallback path.
|
||||
|
||||
## Verification
|
||||
|
||||
- RED: the new focused regression returned 403 for Capability v2 `ls` before the implementation.
|
||||
- `pnpm --filter @knowledge/api exec vitest run src/knowledge-fs-handlers-branch-coverage.test.ts`:
|
||||
passed, 16 tests.
|
||||
- `pnpm --filter @knowledge/api typecheck`: passed.
|
||||
- Targeted Biome check for both changed TypeScript files: passed.
|
||||
- `CI=1 pnpm check`: passed, including the full workspace test suite, coverage gates,
|
||||
retrieval evaluations, migration checks, and deployment/static guards.
|
||||
- `CI=1 pnpm build`: passed for all 12 KnowledgeFS packages.
|
||||
- `CI=1 pnpm lint:backend`: passed, 980 files checked.
|
||||
- `CI=1 pnpm lint`: attempted but remains blocked by 10 pre-existing, unrelated
|
||||
whole-workspace Admin/generated-contract findings. These include formatting in Admin files
|
||||
outside this change and the checked-in 1.5 MiB OpenAPI artifact exceeding Biome's 1 MiB limit.
|
||||
- `uv run --project api python api/dev/generate_knowledge_fs_contract.py --check`: passed after
|
||||
intentionally refreshing the staged KnowledgeFS subtree lock; OpenAPI and Capability v2
|
||||
contract digests were unchanged.
|
||||
|
||||
## Risks and follow-up
|
||||
|
||||
- The change affects authorization plumbing for ten command handlers but does not broaden access:
|
||||
the existing subject, tenant, resource, and content-scope checks are unchanged.
|
||||
- Dify's translation of upstream authorization failures into the generic
|
||||
`knowledge_fs_unavailable` response is separate from this root-cause fix.
|
||||
@ -63,6 +63,31 @@ describe("KnowledgeFS handler branch coverage", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("executes every command with a Capability v2 candidate scope", async () => {
|
||||
const fixture = handlersFixture({
|
||||
capability: capabilityGrant(),
|
||||
decision: undefined,
|
||||
});
|
||||
|
||||
for (const [route, command] of ROUTES) {
|
||||
const response = await fixture.invoke(route);
|
||||
expect(response).toEqual({ body: { command }, status: 200 });
|
||||
}
|
||||
|
||||
expect(fixture.execute).toHaveBeenCalledTimes(ROUTES.length);
|
||||
for (const [, command] of ROUTES) {
|
||||
expect(fixture.execute).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
input: expect.objectContaining({
|
||||
candidatePermissionScope: ["scope:a", "scope:b"],
|
||||
knowledgeSpaceId: SPACE_ID,
|
||||
}),
|
||||
name: command,
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
it("returns not found before executing every command when the space is absent", async () => {
|
||||
const fixture = handlersFixture({ space: null });
|
||||
|
||||
@ -149,6 +174,7 @@ describe("KnowledgeFS handler branch coverage", () => {
|
||||
});
|
||||
|
||||
interface FixtureOptions {
|
||||
readonly capability?: unknown;
|
||||
readonly decision?: unknown;
|
||||
readonly executeError?: Error;
|
||||
readonly space?: unknown;
|
||||
@ -183,6 +209,7 @@ function handlersFixture(options: FixtureOptions = {}) {
|
||||
if (!callback) throw new Error("route was not registered");
|
||||
return callback(
|
||||
context({
|
||||
capability: options.capability,
|
||||
decision:
|
||||
"decision" in options
|
||||
? options.decision
|
||||
@ -200,9 +227,16 @@ function handlersFixture(options: FixtureOptions = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function context({ decision }: { readonly decision: unknown }) {
|
||||
function context({
|
||||
capability,
|
||||
decision,
|
||||
}: {
|
||||
readonly capability: unknown;
|
||||
readonly decision: unknown;
|
||||
}) {
|
||||
const values = new Map<string, unknown>([
|
||||
["authorizationDecision", decision],
|
||||
["capabilityV2Grant", capability],
|
||||
["subject", SUBJECT],
|
||||
["traceId", "trace-1"],
|
||||
]);
|
||||
@ -218,3 +252,12 @@ function context({ decision }: { readonly decision: unknown }) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function capabilityGrant() {
|
||||
return {
|
||||
contentScopeIds: ["scope:b", "scope:a", "scope:a"],
|
||||
namespaceId: SUBJECT.tenantId,
|
||||
resource: { id: SPACE_ID, parent_id: null, type: "knowledge_space" },
|
||||
subject: SUBJECT.subjectId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -65,6 +65,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -125,6 +126,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -185,6 +187,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -245,6 +248,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -305,6 +309,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -355,6 +360,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -402,6 +408,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -449,6 +456,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -496,6 +504,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
@ -560,6 +569,7 @@ export function registerKnowledgeFsHandlers({
|
||||
}
|
||||
|
||||
const candidatePermissionScope = currentCandidateGrants({
|
||||
capabilityGrant: context.get("capabilityV2Grant"),
|
||||
decision: context.get("authorizationDecision"),
|
||||
knowledgeSpaceId: params.id,
|
||||
subject,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user