buster/packages/access-controls/src/chats.ts

164 lines
4.2 KiB
TypeScript

import {
and,
assetPermissions,
assetTypeEnum,
chats,
collectionsToAssets,
eq,
getDb,
identityTypeEnum,
isNull,
usersToOrganizations,
} from '@buster/database';
import { z } from 'zod';
// Input validation schema
const CanUserAccessChatSchema = z.object({
userId: z.string().uuid(),
chatId: z.string().uuid(),
});
export const canUserAccessChat = async ({
userId,
chatId,
}: {
userId: string;
chatId: string;
}): Promise<boolean> => {
// Validate inputs
const input = CanUserAccessChatSchema.parse({ userId, chatId });
const db = getDb();
// Run all permission checks concurrently for optimal performance
const [directPermission, collectionPermission, chatInfo, userOrgs] = await Promise.all([
// Check 1: Direct user permission on chat
checkDirectChatPermission(db, input.userId, input.chatId),
// Check 2: User permission through collections
checkCollectionChatPermission(db, input.userId, input.chatId),
// Check 3: Get chat info (creator & organization)
getChatInfo(db, input.chatId),
// Check 4: Get user's organizations and roles
getUserOrganizations(db, input.userId),
]);
// If chat doesn't exist or is deleted, deny access
if (!chatInfo) {
return false;
}
// Check 1: Direct permission exists
if (directPermission) {
return true;
}
// Check 2: Collection permission exists
if (collectionPermission) {
return true;
}
// Check 3: User is the creator
if (chatInfo.createdBy === input.userId) {
return true;
}
// Check 4: User is workspace_admin or data_admin in the chat's organization
const isOrgAdmin = userOrgs.some(
(org) =>
org.organizationId === chatInfo.organizationId &&
(org.role === 'workspace_admin' || org.role === 'data_admin')
);
return isOrgAdmin;
};
// Helper function to check direct chat permission
async function checkDirectChatPermission(
db: ReturnType<typeof getDb>,
userId: string,
chatId: string
): Promise<boolean> {
const result = await db
.select({ id: assetPermissions.assetId })
.from(assetPermissions)
.where(
and(
eq(assetPermissions.assetId, chatId),
eq(assetPermissions.assetType, 'chat'),
eq(assetPermissions.identityId, userId),
eq(assetPermissions.identityType, 'user'),
isNull(assetPermissions.deletedAt)
)
)
.limit(1);
return result.length > 0;
}
// Helper function to check collection-based chat permission
async function checkCollectionChatPermission(
db: ReturnType<typeof getDb>,
userId: string,
chatId: string
): Promise<boolean> {
const result = await db
.selectDistinct({ collectionId: collectionsToAssets.collectionId })
.from(collectionsToAssets)
.innerJoin(
assetPermissions,
and(
eq(assetPermissions.assetId, collectionsToAssets.collectionId),
eq(assetPermissions.assetType, 'collection'),
eq(assetPermissions.identityId, userId),
eq(assetPermissions.identityType, 'user'),
isNull(assetPermissions.deletedAt)
)
)
.where(
and(
eq(collectionsToAssets.assetId, chatId),
eq(collectionsToAssets.assetType, 'chat'),
isNull(collectionsToAssets.deletedAt)
)
)
.limit(1);
return result.length > 0;
}
// Helper function to get chat info (creator and organization)
async function getChatInfo(
db: ReturnType<typeof getDb>,
chatId: string
): Promise<{ createdBy: string; organizationId: string } | null> {
const result = await db
.select({
createdBy: chats.createdBy,
organizationId: chats.organizationId,
})
.from(chats)
.where(and(eq(chats.id, chatId), isNull(chats.deletedAt)))
.limit(1);
return result[0] || null;
}
// Helper function to get user's organizations and roles
async function getUserOrganizations(
db: ReturnType<typeof getDb>,
userId: string
): Promise<Array<{ organizationId: string; role: string }>> {
const result = await db
.select({
organizationId: usersToOrganizations.organizationId,
role: usersToOrganizations.role,
})
.from(usersToOrganizations)
.where(and(eq(usersToOrganizations.userId, userId), isNull(usersToOrganizations.deletedAt)));
return result;
}