mirror of
https://github.com/formbricks/formbricks.git
synced 2026-05-14 19:38:53 -05:00
201 lines
5.8 KiB
JavaScript
201 lines
5.8 KiB
JavaScript
/* eslint-env es2022 */
|
|
|
|
const TENANT_MEMBERS = ["FeedbackRecords.tenantId", "TopicsUnnested.tenantId"];
|
|
const REQUIRED_SCOPE = "xm:cube:query";
|
|
|
|
function assertRequiredEnvironmentVariable(name) {
|
|
const value = process.env[name];
|
|
|
|
if (typeof value !== "string" || value.trim().length === 0) {
|
|
throw new Error(`${name} is required to run Cube`);
|
|
}
|
|
}
|
|
|
|
assertRequiredEnvironmentVariable("CUBEJS_API_SECRET");
|
|
|
|
function getStringClaim(securityContext, claim) {
|
|
const value = securityContext?.[claim];
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
|
|
const trimmedValue = value.trim();
|
|
return trimmedValue.length > 0 ? trimmedValue : null;
|
|
}
|
|
|
|
function getRequiredStringClaim(securityContext, claim) {
|
|
const value = getStringClaim(securityContext, claim);
|
|
|
|
if (!value) {
|
|
throw new Error(`Cube query rejected: missing ${claim} security context`);
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function collectFilterMembers(filters) {
|
|
if (!Array.isArray(filters)) {
|
|
return [];
|
|
}
|
|
|
|
return filters.flatMap((filter) => [
|
|
...(typeof filter?.member === "string" ? [filter.member] : []),
|
|
...(typeof filter?.dimension === "string" ? [filter.dimension] : []),
|
|
...collectFilterMembers(filter?.and),
|
|
...collectFilterMembers(filter?.or),
|
|
]);
|
|
}
|
|
|
|
function collectOrderMembers(order) {
|
|
if (!order) {
|
|
return [];
|
|
}
|
|
|
|
if (Array.isArray(order)) {
|
|
return order
|
|
.map((orderEntry) => (Array.isArray(orderEntry) ? orderEntry[0] : null))
|
|
.filter((member) => typeof member === "string");
|
|
}
|
|
|
|
if (typeof order === "object") {
|
|
return Object.keys(order);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
function collectTimeDimensionMembers(timeDimensions) {
|
|
if (!Array.isArray(timeDimensions)) {
|
|
return [];
|
|
}
|
|
|
|
return timeDimensions
|
|
.map((timeDimension) => timeDimension?.dimension)
|
|
.filter((dimension) => typeof dimension === "string");
|
|
}
|
|
|
|
function collectQueryMembers(query) {
|
|
const cubeQuery = query ?? {};
|
|
const members = [
|
|
...(Array.isArray(cubeQuery.measures) ? cubeQuery.measures : []),
|
|
...(Array.isArray(cubeQuery.dimensions) ? cubeQuery.dimensions : []),
|
|
...(Array.isArray(cubeQuery.segments) ? cubeQuery.segments : []),
|
|
...collectTimeDimensionMembers(cubeQuery.timeDimensions),
|
|
...collectFilterMembers(cubeQuery.filters),
|
|
...collectOrderMembers(cubeQuery.order),
|
|
].filter((member) => typeof member === "string");
|
|
|
|
return Array.from(new Set(members)).sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function assertValidSecurityContext(securityContext) {
|
|
const tenantId = getRequiredStringClaim(securityContext, "tenantId");
|
|
const feedbackDirectoryId = getRequiredStringClaim(securityContext, "feedbackDirectoryId");
|
|
const workspaceId = getRequiredStringClaim(securityContext, "workspaceId");
|
|
const scope = getRequiredStringClaim(securityContext, "scope");
|
|
|
|
if (scope !== REQUIRED_SCOPE) {
|
|
throw new Error("Cube query rejected: invalid Cube query scope");
|
|
}
|
|
if (tenantId !== feedbackDirectoryId) {
|
|
throw new Error("Cube query rejected: tenantId/feedbackDirectoryId mismatch");
|
|
}
|
|
|
|
return {
|
|
tenantId,
|
|
feedbackDirectoryId,
|
|
workspaceId,
|
|
organizationId: getRequiredStringClaim(securityContext, "organizationId"),
|
|
userId: getRequiredStringClaim(securityContext, "userId"),
|
|
requestId: getRequiredStringClaim(securityContext, "jti"),
|
|
source: getRequiredStringClaim(securityContext, "source"),
|
|
};
|
|
}
|
|
|
|
function assertNoCallerTenantMember(query) {
|
|
for (const member of collectQueryMembers(query)) {
|
|
if (TENANT_MEMBERS.includes(member)) {
|
|
throw new Error("Cube query rejected: tenant filters are enforced by Cube");
|
|
}
|
|
}
|
|
}
|
|
|
|
function logCubeQueryAuditEvent(context, query, { error, status = "success" } = {}) {
|
|
const errorName = error instanceof Error ? error.name : undefined;
|
|
const errorMessage = error instanceof Error ? error.message : error ? String(error) : undefined;
|
|
|
|
console.log(
|
|
JSON.stringify({
|
|
type: "audit",
|
|
event: "cube.query",
|
|
status,
|
|
timestamp: new Date().toISOString(),
|
|
tenantId: context.tenantId,
|
|
feedbackDirectoryId: context.feedbackDirectoryId,
|
|
workspaceId: context.workspaceId,
|
|
organizationId: context.organizationId,
|
|
userId: context.userId,
|
|
requestId: context.requestId,
|
|
source: context.source,
|
|
members: collectQueryMembers(query),
|
|
...(errorName ? { errorName } : {}),
|
|
...(errorMessage ? { errorMessage } : {}),
|
|
})
|
|
);
|
|
}
|
|
|
|
function logCubeQuerySecurityContextFailure(query, error) {
|
|
console.log(
|
|
JSON.stringify({
|
|
type: "audit",
|
|
event: "cube.query",
|
|
status: "failure",
|
|
timestamp: new Date().toISOString(),
|
|
members: collectQueryMembers(query),
|
|
errorName: error instanceof Error ? error.name : undefined,
|
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
})
|
|
);
|
|
}
|
|
|
|
function queryRewrite(query, rewriteContext) {
|
|
const cubeQuery = query ?? {};
|
|
let context;
|
|
|
|
try {
|
|
context = assertValidSecurityContext(rewriteContext?.securityContext);
|
|
} catch (error) {
|
|
logCubeQuerySecurityContextFailure(cubeQuery, error);
|
|
throw error;
|
|
}
|
|
|
|
try {
|
|
assertNoCallerTenantMember(cubeQuery);
|
|
} catch (error) {
|
|
logCubeQueryAuditEvent(context, cubeQuery, { error, status: "failure" });
|
|
throw error;
|
|
}
|
|
|
|
const queriedCubePrefixes = new Set(collectQueryMembers(cubeQuery).map((member) => member.split(".")[0]));
|
|
const rewrittenQuery = {
|
|
...cubeQuery,
|
|
filters: [
|
|
...(Array.isArray(cubeQuery.filters) ? cubeQuery.filters : []),
|
|
...TENANT_MEMBERS.filter((member) => queriedCubePrefixes.has(member.split(".")[0])).map(
|
|
(member) => ({
|
|
member,
|
|
operator: "equals",
|
|
values: [context.tenantId],
|
|
})
|
|
),
|
|
],
|
|
};
|
|
|
|
logCubeQueryAuditEvent(context, rewrittenQuery);
|
|
return rewrittenQuery;
|
|
}
|
|
|
|
module.exports = {
|
|
queryRewrite,
|
|
};
|