/** * Обертка для создания безопасных GraphQL резолверов * * Обеспечивает автоматическую проверку прав доступа, * фильтрацию данных и аудит для резолверов */ import { OrganizationType, PrismaClient } from '@prisma/client' import { GraphQLError } from 'graphql' import { SecurityLogger } from '../../lib/security-logger' import { CommercialDataAudit } from './commercial-data-audit' import { ParticipantIsolation } from './participant-isolation' import { SupplyDataFilter } from './supply-data-filter' import { SecurityContext, CommercialAccessType, ResourceType } from './types' /** * Опции для создания безопасного резолвера */ interface SecureResolverOptions { resourceType: ResourceType requiredRole?: OrganizationType[] auditAction: CommercialAccessType enableFiltering?: boolean enableAudit?: boolean } /** * Контекст GraphQL с добавленными полями безопасности */ interface GraphQLContext { user?: { id: string organizationId: string organizationType: OrganizationType } prisma: unknown // PrismaClient req?: { ip?: string headers?: Record socket?: { remoteAddress?: string } } } /** * Создает безопасную обертку для GraphQL резолвера */ export function createSecureResolver( resolver: (parent: unknown, args: TArgs, context: GraphQLContext) => Promise, options: SecureResolverOptions, ) { return async (parent: unknown, args: TArgs, context: GraphQLContext): Promise => { const securityEnabled = process.env.ENABLE_SUPPLY_SECURITY === 'true' const auditEnabled = process.env.ENABLE_SECURITY_AUDIT === 'true' // Если система безопасности отключена - выполняем оригинальный резолвер if (!securityEnabled) { return resolver(parent, args, context) } // Проверка аутентификации if (!context.user) { throw new GraphQLError('Authentication required', { extensions: { code: 'UNAUTHENTICATED' }, }) } const securityContext: SecurityContext = { user: { id: context.user.id, organizationId: context.user.organizationId, organizationType: context.user.organizationType, }, ipAddress: context.req?.ip || context.req?.socket?.remoteAddress, userAgent: context.req?.headers?.['user-agent'], request: { headers: context.req?.headers || {}, timestamp: new Date(), }, } try { // Проверка роли если требуется if (options.requiredRole && !options.requiredRole.includes(context.user.organizationType)) { // Логируем попытку несанкционированного доступа if (auditEnabled) { await CommercialDataAudit.logUnauthorizedAccess(context.prisma as any, { userId: context.user.id, organizationType: context.user.organizationType, resourceType: options.resourceType, resourceId: 'unknown', reason: `Insufficient role: ${context.user.organizationType}, required: ${options.requiredRole.join(', ')}`, ipAddress: securityContext.ipAddress, userAgent: securityContext.userAgent, }) } throw new GraphQLError('Insufficient permissions', { extensions: { code: 'FORBIDDEN' }, }) } // Логирование доступа if (auditEnabled && options.enableAudit !== false) { await CommercialDataAudit.logAccess(context.prisma as any, { userId: securityContext.user.id, organizationType: securityContext.user.organizationType, action: options.auditAction, resourceType: options.resourceType, metadata: { args }, ipAddress: securityContext.ipAddress, userAgent: securityContext.userAgent, }) } // Выполнение оригинального резолвера let result = await resolver(parent, args, context) // Фильтрация результата если включена if (options.enableFiltering !== false && result) { result = await filterResolverResult(result, securityContext, options.resourceType) } return result } catch (error) { SecurityLogger.logSecurityError(error as Error, { operation: 'secureResolver', resourceType: options.resourceType, userId: context.user.id, organizationType: context.user.organizationType, }) throw error } } } /** * Фильтрует результат резолвера в зависимости от типа данных */ async function filterResolverResult( result: unknown, context: SecurityContext, resourceType: ResourceType, ): Promise { // Если это массив - фильтруем каждый элемент if (Array.isArray(result)) { return Promise.all(result.map((item) => filterSingleItem(item, context, resourceType))) } // Если это одиночный объект - фильтруем его return filterSingleItem(result, context, resourceType) } /** * Фильтрует одиночный элемент данных */ async function filterSingleItem(item: unknown, context: SecurityContext, resourceType: ResourceType): Promise { switch (resourceType) { case 'SUPPLY_ORDER': // Фильтруем данные поставки if (item && typeof item === 'object' && (item as any).id) { const filtered = SupplyDataFilter.filterSupplyOrder(item as any, context) return filtered.data } break case 'PRODUCT': case 'SERVICE': case 'CONSUMABLE': // Для других типов ресурсов - пока возвращаем как есть // TODO: добавить специфичную фильтрацию break } return item } /** * Декоратор для автоматического создания безопасного резолвера */ export function SecureResolver(options: SecureResolverOptions) { return function (_target: unknown, _propertyName: string, descriptor: PropertyDescriptor) { const method = descriptor.value descriptor.value = createSecureResolver(method, options) } } /** * Вспомогательные функции для проверки доступа */ export const SecurityHelpers = { /** * Проверяет доступ к заказу поставки */ async checkSupplyOrderAccess(prisma: unknown, orderId: string, context: SecurityContext): Promise { return ParticipantIsolation.validateSupplyOrderAccess(prisma as PrismaClient, orderId, context) }, /** * Проверяет партнерские отношения */ async checkPartnershipAccess( prisma: unknown, organizationId: string, partnerId: string, context: SecurityContext, ): Promise { return ParticipantIsolation.validatePartnerAccess(prisma as PrismaClient, organizationId, partnerId, context) }, }