151 lines
6 KiB
Django/Jinja
151 lines
6 KiB
Django/Jinja
apiVersion: v1
|
|
kind: ConfigMap
|
|
metadata:
|
|
name: {{ taskserv.name }}-cube-config
|
|
namespace: {{ taskserv.namespace }}
|
|
labels:
|
|
app.kubernetes.io/name: {{ taskserv.name }}-cube
|
|
app.kubernetes.io/managed-by: provisioning
|
|
data:
|
|
cube.js: |
|
|
/* eslint-env es2022 */
|
|
|
|
const TENANT_MEMBERS = ["FeedbackRecords.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((e) => (Array.isArray(e) ? e[0] : null)).filter((m) => typeof m === "string");
|
|
}
|
|
if (typeof order === "object") return Object.keys(order);
|
|
return [];
|
|
}
|
|
|
|
function collectTimeDimensionMembers(timeDimensions) {
|
|
if (!Array.isArray(timeDimensions)) return [];
|
|
return timeDimensions.map((td) => td?.dimension).filter((d) => typeof d === "string");
|
|
}
|
|
|
|
function collectQueryMembers(query) {
|
|
const q = query ?? {};
|
|
const members = [
|
|
...(Array.isArray(q.measures) ? q.measures : []),
|
|
...(Array.isArray(q.dimensions) ? q.dimensions : []),
|
|
...(Array.isArray(q.segments) ? q.segments : []),
|
|
...collectTimeDimensionMembers(q.timeDimensions),
|
|
...collectFilterMembers(q.filters),
|
|
...collectOrderMembers(q.order),
|
|
].filter((m) => typeof m === "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((m) => m.split(".")[0]));
|
|
const rewrittenQuery = {
|
|
...cubeQuery,
|
|
filters: [
|
|
...(Array.isArray(cubeQuery.filters) ? cubeQuery.filters : []),
|
|
...TENANT_MEMBERS.filter((m) => queriedCubePrefixes.has(m.split(".")[0])).map((member) => ({
|
|
member, operator: "equals", values: [context.tenantId],
|
|
})),
|
|
],
|
|
};
|
|
logCubeQueryAuditEvent(context, rewrittenQuery);
|
|
return rewrittenQuery;
|
|
}
|
|
|
|
module.exports = { queryRewrite };
|