openclaw / src /gateway /server-methods /artifacts-session-resolution.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
78d0c37 verified
Raw History Blame Contribute Delete
8.25 kB
import { beforeEach, describe, expect, it, vi } from "vitest";
import { upsertSessionEntryCore } from "../../config/sessions/session-accessor.js";
import { ensureProfileForEmail } from "../../state/user-profiles.js";
import { withOpenClawTestState } from "../../test-utils/openclaw-test-state.js";
import {
roleClient,
rolePolicyConfig,
sharingPolicyClient,
} from "../session-sharing.test-utils.js";
import {
ArtifactSessionResolutionError,
resolveAuthorizedArtifactSession,
} from "./artifacts-session-resolution.js";
import type { GatewayClient } from "./types.js";
const mocks = vi.hoisted(() => ({
getTaskSession: vi.fn(),
resolveRunSession: vi.fn(),
}));
vi.mock("../../tasks/task-status-access.js", () => ({
getTaskSessionLookupByIdForStatus: mocks.getTaskSession,
}));
vi.mock("../server-session-key.js", () => ({
resolveSessionKeyForRun: mocks.resolveRunSession,
}));
function identifiedClient(scopes: string[], profileId = "viewer@example.com"): GatewayClient {
return {
connect: {
minProtocol: 1,
maxProtocol: 1,
client: { id: "openclaw-control-ui", version: "test", platform: "test", mode: "webchat" },
role: "operator",
scopes,
},
authenticatedUserId: "viewer@example.com",
authenticatedUserProfile: {
profileId,
displayName: null,
hasAvatar: false,
updatedAt: 1,
},
};
}
describe("artifact session authorization", () => {
beforeEach(() => vi.clearAllMocks());
it("denies direct and indirect incognito selectors while preserving admin access", async () => {
await withOpenClawTestState({ scenario: "minimal" }, async () => {
const sessionKey = "agent:main:dashboard:incognito-artifacts";
const cfg = { agents: { list: [{ id: "main", default: true }] } };
await upsertSessionEntryCore(
{ agentId: "main", sessionKey },
{
sessionId: "session-incognito-artifacts",
updatedAt: 1,
incognito: true,
visibility: "shared",
},
);
mocks.getTaskSession.mockReturnValue({
requesterSessionKey: sessionKey,
requesterAgentId: "main",
ownerKey: sessionKey,
});
mocks.resolveRunSession.mockReturnValue(sessionKey);
const viewer = identifiedClient(["operator.read"]);
expect(() =>
resolveAuthorizedArtifactSession(
{ sessionKey: "dashboard:incognito-artifacts", agentId: "main" },
cfg,
viewer,
),
).toThrow('Incognito session "dashboard:incognito-artifacts" was not found.');
for (const query of [{ taskId: "task-private" }, { runId: "run-private" }]) {
try {
resolveAuthorizedArtifactSession(query, cfg, viewer);
throw new Error("expected incognito artifact selector to be denied");
} catch (error) {
expect(error).toBeInstanceOf(ArtifactSessionResolutionError);
expect((error as ArtifactSessionResolutionError).shape).toMatchObject({
message: "no session found for artifact query",
details: { type: "artifact_scope_not_found" },
});
}
}
expect(
resolveAuthorizedArtifactSession(
{ sessionKey: "dashboard:incognito-artifacts", agentId: "main" },
cfg,
identifiedClient(["operator.admin"]),
),
).toMatchObject({ sessionKey });
});
});
it.each([
{ name: "draft without roles", visibility: "draft", cfg: {}, role: undefined },
{ name: "draft without runtime config", visibility: "draft", cfg: undefined, role: undefined },
{
name: "shared with a none role",
visibility: "shared",
cfg: rolePolicyConfig(),
role: "none",
},
{
name: "draft with a write role",
visibility: "draft",
cfg: rolePolicyConfig(),
role: "write",
},
] as const)(
"hides $name artifacts behind direct, run, and task selectors",
async ({ visibility, cfg, role }) => {
await withOpenClawTestState({ scenario: "minimal" }, async () => {
const viewerProfile = ensureProfileForEmail("viewer@example.com");
const ownerProfile = ensureProfileForEmail("owner@example.com");
const sessionKey = "agent:main:foreign-artifacts";
await upsertSessionEntryCore(
{ agentId: "main", sessionKey },
{
sessionId: "session-foreign-artifacts",
updatedAt: 1,
createdActor: { type: "human", source: "profile", id: ownerProfile.id },
visibility,
},
);
mocks.getTaskSession.mockImplementation((taskId: string) =>
taskId === "task-run"
? { runId: "run-foreign", agentId: "main" }
: {
requesterSessionKey: sessionKey,
requesterAgentId: "main",
ownerKey: sessionKey,
},
);
mocks.resolveRunSession.mockReturnValue(sessionKey);
const viewer = role
? roleClient(role, "artifact-viewer")
: identifiedClient(["operator.read"], viewerProfile.id);
for (const query of [
{ sessionKey },
{ runId: "run-foreign" },
{ taskId: "task-foreign" },
{ taskId: "task-run" },
]) {
expect(() => resolveAuthorizedArtifactSession(query, cfg, viewer)).toThrowError(
expect.objectContaining({
shape: {
code: "INVALID_REQUEST",
message: "no session found for artifact query",
details: { type: "artifact_scope_not_found" },
},
}),
);
}
expect(
resolveAuthorizedArtifactSession(
{ sessionKey },
cfg,
identifiedClient(["operator.read"], ownerProfile.id),
),
).toMatchObject({ sessionKey });
expect(
resolveAuthorizedArtifactSession(
{ runId: "run-foreign" },
cfg,
identifiedClient(["operator.admin"], viewerProfile.id),
),
).toMatchObject({ sessionKey });
});
},
);
it.each(["shared", "read-only", "suggest"] as const)(
"allows reading foreign %s artifacts without participation",
async (visibility) => {
await withOpenClawTestState({ scenario: "minimal" }, async () => {
const owner = ensureProfileForEmail("artifact-owner@example.test");
const viewer = roleClient("view", "artifact-reader");
const sessionKey = "agent:main:readable-artifacts";
await upsertSessionEntryCore(
{ agentId: "main", sessionKey },
{
sessionId: "session-readable-artifacts",
updatedAt: 1,
createdActor: { type: "human", source: "profile", id: owner.id },
visibility,
},
);
for (const cfg of [undefined, {}, rolePolicyConfig()]) {
expect(resolveAuthorizedArtifactSession({ sessionKey }, cfg, viewer)).toMatchObject({
sessionKey,
});
}
});
},
);
it("preserves solo and system reads while denying identityless reads with roles", async () => {
await withOpenClawTestState({ scenario: "minimal" }, async () => {
const sessionKey = "agent:main:solo-artifacts";
await upsertSessionEntryCore(
{ agentId: "main", sessionKey },
{ sessionId: "session-solo-artifacts", updatedAt: 1, visibility: "draft" },
);
const identityless = sharingPolicyClient({});
for (const client of [null, identityless, sharingPolicyClient({ user: "gateway-owner" })]) {
expect(resolveAuthorizedArtifactSession({ sessionKey }, {}, client)).toMatchObject({
sessionKey,
});
}
const cfg = rolePolicyConfig();
expect(() => resolveAuthorizedArtifactSession({ sessionKey }, cfg, identityless)).toThrow(
"no session found for artifact query",
);
const system: GatewayClient = {
...identityless,
internal: { operatorRoleActor: { kind: "system" } },
};
expect(resolveAuthorizedArtifactSession({ sessionKey }, cfg, system)).toMatchObject({
sessionKey,
});
});
});
});