mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
Auth cleanup
This commit is contained in:
parent
de381f909c
commit
0f2b3f7010
4 changed files with 115 additions and 72 deletions
|
|
@ -64,6 +64,59 @@ export const captureEvent = async ({ event, ...rest }: AnalyticsEvent) => {
|
|||
await analytics.insert({ table, values: [value], format: 'JSONEachRow' });
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to build access control filters for analytics queries
|
||||
* Includes both organization-level and user-level filtering
|
||||
*/
|
||||
type AccessControlResult = {
|
||||
accessFilter: string;
|
||||
accessParams: Record<string, string>;
|
||||
};
|
||||
|
||||
const buildAccessControlFilter = (
|
||||
authUserId: string | null,
|
||||
isAdmin: boolean,
|
||||
orgId: string | null | undefined,
|
||||
authOrgId: string | null,
|
||||
tablePrefix: string = '',
|
||||
): AccessControlResult => {
|
||||
// For personal accounts, query by userId instead of orgId
|
||||
if (!orgId && !authUserId) {
|
||||
throw new Error('Personal accounts require authenticated user ID');
|
||||
}
|
||||
|
||||
const accessParams: Record<string, string> = {};
|
||||
const prefix = tablePrefix ? `${tablePrefix}.` : '';
|
||||
|
||||
// Build organization scope filter first
|
||||
const orgCondition = !orgId
|
||||
? `${prefix}orgId IS NULL`
|
||||
: `${prefix}orgId = {orgId: String}`;
|
||||
|
||||
if (orgId) {
|
||||
accessParams.orgId = orgId;
|
||||
}
|
||||
|
||||
// Build access control: (userId = your_id) OR (you're admin AND orgId = your_org)
|
||||
let accessCondition = '';
|
||||
if (authUserId) {
|
||||
accessParams.authUserId = authUserId;
|
||||
|
||||
if (orgId && isAdmin && authOrgId) {
|
||||
// Admin can see: their own tasks OR all tasks in their org
|
||||
accessParams.authOrgId = authOrgId;
|
||||
accessCondition = ` AND (${prefix}userId = {authUserId: String} OR ${prefix}orgId = {authOrgId: String})`;
|
||||
} else {
|
||||
// Non-admin or personal account: only see your own tasks
|
||||
accessCondition = ` AND ${prefix}userId = {authUserId: String}`;
|
||||
}
|
||||
}
|
||||
|
||||
const accessFilter = `${orgCondition}${accessCondition}`;
|
||||
|
||||
return { accessFilter, accessParams };
|
||||
};
|
||||
|
||||
/**
|
||||
* getUsage
|
||||
*/
|
||||
|
|
@ -89,33 +142,28 @@ export const getUsage = async ({
|
|||
timePeriod?: AnyTimePeriod;
|
||||
userId?: string | null;
|
||||
}): Promise<UsageRecord> => {
|
||||
const { effectiveUserId } = await authorizeAnalytics({
|
||||
const { authUserId, authOrgId, isAdmin } = await authorizeAnalytics({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
});
|
||||
|
||||
// For personal accounts, query by userId instead of orgId
|
||||
if (!orgId && !effectiveUserId) {
|
||||
if (!orgId && !authUserId) {
|
||||
return {}; // Personal accounts must have a userId
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
|
||||
const { accessFilter, accessParams } = buildAccessControlFilter(
|
||||
authUserId,
|
||||
isAdmin,
|
||||
orgId,
|
||||
authOrgId,
|
||||
);
|
||||
|
||||
// Build query conditions based on account type
|
||||
const orgCondition = !orgId ? 'orgId IS NULL' : 'orgId = {orgId: String}';
|
||||
|
||||
const queryParams: Record<string, string | number> = {
|
||||
const queryParams = {
|
||||
timePeriod,
|
||||
...accessParams,
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
queryParams.orgId = orgId;
|
||||
}
|
||||
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
}
|
||||
|
||||
const results = await analytics.query({
|
||||
query: `
|
||||
SELECT
|
||||
|
|
@ -126,9 +174,8 @@ export const getUsage = async ({
|
|||
SUM(COALESCE(cost, 0)) AS cost
|
||||
FROM events
|
||||
WHERE
|
||||
${orgCondition}
|
||||
${accessFilter}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
${userFilter}
|
||||
GROUP BY 1
|
||||
`,
|
||||
format: 'JSONEachRow',
|
||||
|
|
@ -425,7 +472,6 @@ export const getTasks = async ({
|
|||
orgId,
|
||||
userId,
|
||||
taskId,
|
||||
allowCrossUserAccess = false,
|
||||
skipAuth = false,
|
||||
limit = 20,
|
||||
cursor,
|
||||
|
|
@ -434,41 +480,56 @@ export const getTasks = async ({
|
|||
orgId?: string | null;
|
||||
userId?: string | null;
|
||||
taskId?: string | null;
|
||||
allowCrossUserAccess?: boolean;
|
||||
skipAuth?: boolean;
|
||||
limit?: number;
|
||||
cursor?: number;
|
||||
filters?: Filter[];
|
||||
}): Promise<TasksResult> => {
|
||||
let effectiveUserId = userId;
|
||||
let authUserId: string | null = null;
|
||||
let authOrgId: string | null = null;
|
||||
let isAdmin = false;
|
||||
|
||||
if (!skipAuth) {
|
||||
const authResult = await authorizeAnalytics({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
allowCrossUserAccess,
|
||||
});
|
||||
effectiveUserId = authResult.effectiveUserId;
|
||||
authUserId = authResult.authUserId;
|
||||
authOrgId = authResult.authOrgId;
|
||||
isAdmin = authResult.isAdmin;
|
||||
}
|
||||
|
||||
// For personal accounts, query by userId instead of orgId
|
||||
// Exception: when skipAuth is true (for public shares), we can query without userId
|
||||
if (!orgId && !effectiveUserId && !skipAuth) {
|
||||
if (!orgId && !authUserId && !skipAuth) {
|
||||
return { tasks: [], hasMore: false }; // Personal accounts must have a userId unless we're skipping auth
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND e.userId = {userId: String}' : '';
|
||||
const taskFilter = taskId ? 'AND e.taskId = {taskId: String}' : '';
|
||||
|
||||
const messageUserFilter = effectiveUserId
|
||||
? 'AND userId = {userId: String}'
|
||||
: '';
|
||||
|
||||
const messageTaskFilter = taskId ? 'AND taskId = {taskId: String}' : '';
|
||||
|
||||
// Build access control conditions based on the two rules:
|
||||
// 1. You always have access to your own tasks
|
||||
// 2. If you're an org:admin, you have access to all tasks in your org
|
||||
let accessFilter = '';
|
||||
let messageAccessFilter = '';
|
||||
|
||||
if (!skipAuth && authUserId) {
|
||||
if (orgId && isAdmin) {
|
||||
// Admin can see all tasks in the org OR their own tasks
|
||||
accessFilter =
|
||||
'AND (e.userId = {authUserId: String} OR (e.orgId = {authOrgId: String}))';
|
||||
messageAccessFilter =
|
||||
'AND (userId = {authUserId: String} OR (orgId = {authOrgId: String}))';
|
||||
} else {
|
||||
// Non-admin or personal account: only see your own tasks
|
||||
accessFilter = 'AND e.userId = {authUserId: String}';
|
||||
messageAccessFilter = 'AND userId = {authUserId: String}';
|
||||
}
|
||||
}
|
||||
|
||||
// Build query conditions based on account type
|
||||
const orgCondition = !orgId ? 'e.orgId IS NULL' : 'e.orgId = {orgId: String}';
|
||||
|
||||
const messageOrgCondition = !orgId
|
||||
? 'orgId IS NULL'
|
||||
: 'orgId = {orgId: String}';
|
||||
|
|
@ -486,8 +547,12 @@ export const getTasks = async ({
|
|||
queryParams.orgId = orgId;
|
||||
}
|
||||
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
if (authOrgId) {
|
||||
queryParams.authOrgId = authOrgId;
|
||||
}
|
||||
|
||||
if (authUserId) {
|
||||
queryParams.authUserId = authUserId;
|
||||
}
|
||||
|
||||
if (taskId) {
|
||||
|
|
@ -518,7 +583,7 @@ export const getTasks = async ({
|
|||
argMin(mode, ts) as mode
|
||||
FROM messages
|
||||
WHERE ${messageOrgCondition}
|
||||
${messageUserFilter}
|
||||
${messageAccessFilter}
|
||||
${messageTaskFilter}
|
||||
GROUP BY taskId
|
||||
)
|
||||
|
|
@ -542,7 +607,7 @@ export const getTasks = async ({
|
|||
${orgCondition}
|
||||
AND e.type IN ({types: Array(String)})
|
||||
AND e.modelId IS NOT NULL
|
||||
${userFilter}
|
||||
${accessFilter}
|
||||
${taskFilter}
|
||||
${filterClause}
|
||||
GROUP BY 1, 2
|
||||
|
|
@ -606,38 +671,33 @@ export const getHourlyUsageByUser = async ({
|
|||
userId?: string | null;
|
||||
filters?: Filter[];
|
||||
}): Promise<HourlyUsageByUser[]> => {
|
||||
const { effectiveUserId } = await authorizeAnalytics({
|
||||
const { authUserId, authOrgId, isAdmin } = await authorizeAnalytics({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
});
|
||||
|
||||
// For personal accounts, query by userId instead of orgId
|
||||
if (!orgId && !effectiveUserId) {
|
||||
if (!orgId && !authUserId) {
|
||||
return []; // Personal accounts must have a userId
|
||||
}
|
||||
|
||||
const userFilter = effectiveUserId ? 'AND userId = {userId: String}' : '';
|
||||
const { accessFilter, accessParams } = buildAccessControlFilter(
|
||||
authUserId,
|
||||
isAdmin,
|
||||
orgId,
|
||||
authOrgId,
|
||||
);
|
||||
|
||||
// Build query conditions based on account type
|
||||
const orgCondition = !orgId ? 'orgId IS NULL' : 'orgId = {orgId: String}';
|
||||
|
||||
const queryParams: Record<string, string | number | string[]> = {
|
||||
const queryParams = {
|
||||
timePeriod,
|
||||
types: [
|
||||
TelemetryEventName.TASK_CREATED,
|
||||
TelemetryEventName.TASK_COMPLETED,
|
||||
TelemetryEventName.LLM_COMPLETION,
|
||||
],
|
||||
...accessParams,
|
||||
};
|
||||
|
||||
if (orgId) {
|
||||
queryParams.orgId = orgId;
|
||||
}
|
||||
|
||||
if (effectiveUserId) {
|
||||
queryParams.userId = effectiveUserId;
|
||||
}
|
||||
|
||||
// Build filter conditions using shared helper
|
||||
const filterClause = buildFilterConditions(filters, queryParams);
|
||||
|
||||
|
|
@ -651,10 +711,9 @@ export const getHourlyUsageByUser = async ({
|
|||
SUM(CASE WHEN type = '${TelemetryEventName.LLM_COMPLETION}' THEN COALESCE(cost, 0) ELSE 0 END) AS cost
|
||||
FROM events
|
||||
WHERE
|
||||
${orgCondition}
|
||||
${accessFilter}
|
||||
AND timestamp >= toUnixTimestamp(now() - INTERVAL {timePeriod: Int32} DAY)
|
||||
AND type IN ({types: Array(String)})
|
||||
${userFilter}
|
||||
${filterClause}
|
||||
GROUP BY 1, 2
|
||||
ORDER BY hour_utc DESC, userId
|
||||
|
|
@ -691,7 +750,6 @@ export const getTaskById = async ({
|
|||
orgId,
|
||||
userId,
|
||||
limit: 1,
|
||||
allowCrossUserAccess: true, // Allow access to tasks from other users in the same org
|
||||
});
|
||||
|
||||
return result.tasks[0] || null;
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ export const getMessages = async (
|
|||
await authorizeAnalytics({
|
||||
requestedOrgId: orgId,
|
||||
requestedUserId: userId,
|
||||
allowCrossUserAccess: true, // Allow viewing messages within authorized tasks
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -72,12 +72,10 @@ export async function authorizeAnalytics({
|
|||
requestedOrgId,
|
||||
requestedUserId,
|
||||
requireAdmin = false,
|
||||
allowCrossUserAccess = false,
|
||||
}: {
|
||||
requestedOrgId?: string | null;
|
||||
requestedUserId?: string | null;
|
||||
requireAdmin?: boolean;
|
||||
allowCrossUserAccess?: boolean;
|
||||
}) {
|
||||
const { orgId: authOrgId, orgRole, userId: authUserId } = await auth();
|
||||
|
||||
|
|
@ -85,6 +83,7 @@ export async function authorizeAnalytics({
|
|||
throw new Error('Unauthorized: User required');
|
||||
}
|
||||
|
||||
// Personal account access
|
||||
if (!authOrgId && !requestedOrgId) {
|
||||
if (requestedUserId && requestedUserId !== authUserId) {
|
||||
throw new Error(
|
||||
|
|
@ -96,10 +95,11 @@ export async function authorizeAnalytics({
|
|||
authOrgId: null,
|
||||
authUserId,
|
||||
orgRole: null,
|
||||
effectiveUserId: authUserId,
|
||||
isAdmin: false,
|
||||
};
|
||||
}
|
||||
|
||||
// Organization account access
|
||||
if (!authOrgId || !authUserId || authOrgId !== requestedOrgId) {
|
||||
throw new Error('Unauthorized: Invalid organization access');
|
||||
}
|
||||
|
|
@ -108,24 +108,13 @@ export async function authorizeAnalytics({
|
|||
throw new Error('Unauthorized: Administrator access required');
|
||||
}
|
||||
|
||||
if (
|
||||
orgRole !== 'org:admin' &&
|
||||
requestedUserId &&
|
||||
requestedUserId !== authUserId
|
||||
) {
|
||||
throw new Error('Unauthorized: Members can only access their own data');
|
||||
}
|
||||
|
||||
const effectiveUserId =
|
||||
orgRole !== 'org:admin' && !allowCrossUserAccess
|
||||
? authUserId
|
||||
: requestedUserId || null;
|
||||
const isAdmin = orgRole === 'org:admin';
|
||||
|
||||
return {
|
||||
authOrgId,
|
||||
authUserId,
|
||||
orgRole: isOrgRole(orgRole) ? orgRole : 'org:member',
|
||||
effectiveUserId,
|
||||
isAdmin,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,6 @@ export async function canShareTask(taskId: string): Promise<{
|
|||
const result = await getTasks({
|
||||
taskId,
|
||||
orgId,
|
||||
allowCrossUserAccess: true,
|
||||
});
|
||||
|
||||
const task = result.tasks[0];
|
||||
|
|
@ -298,7 +297,6 @@ export async function getTaskByShareToken(token: string): Promise<{
|
|||
const result = await getTasks({
|
||||
taskId: share.taskId,
|
||||
orgId: share.orgId, // Will be null for personal shares
|
||||
allowCrossUserAccess: true,
|
||||
skipAuth: true, // Skip auth for both public and organization shares since we've already validated access above
|
||||
});
|
||||
const task = result.tasks[0];
|
||||
|
|
@ -531,7 +529,6 @@ export async function getSharedTaskMessages(
|
|||
const result = await getTasks({
|
||||
taskId: share.taskId,
|
||||
orgId: share.orgId,
|
||||
allowCrossUserAccess: true,
|
||||
skipAuth: true, // We've already validated access above
|
||||
});
|
||||
const task = result.tasks[0];
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue